/* * Copyright 2006-2010 Daniel Henninger. All rights reserved. * * This software is published under the terms of the GNU Public License (GPL), * a copy of which is included in this distribution. */ package net.sf.kraken; import net.sf.kraken.avatars.Avatar; import net.sf.kraken.muc.BaseMUCTransport; import net.sf.kraken.permissions.PermissionManager; import net.sf.kraken.registration.Registration; import net.sf.kraken.registration.RegistrationHandler; import net.sf.kraken.registration.RegistrationManager; import net.sf.kraken.roster.TransportBuddy; import net.sf.kraken.session.TransportSession; import net.sf.kraken.session.TransportSessionManager; import net.sf.kraken.session.cluster.TransportSessionRouter; import net.sf.kraken.type.*; import net.sf.kraken.util.chatstate.ChatStateChangeEvent; import net.sf.kraken.util.chatstate.ChatStateEventListener; import net.sf.kraken.util.chatstate.ChatStateEventSource; import org.apache.log4j.Logger; import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.QName; import org.jivesoftware.openfire.RemotePacketRouter; import org.jivesoftware.openfire.SessionManager; import org.jivesoftware.openfire.XMPPServer; import org.jivesoftware.openfire.auth.UnauthorizedException; import org.jivesoftware.openfire.container.PluginManager; import org.jivesoftware.openfire.event.SessionEventDispatcher; import org.jivesoftware.openfire.event.SessionEventListener; import org.jivesoftware.openfire.event.UserEventDispatcher; import org.jivesoftware.openfire.event.UserEventListener; import org.jivesoftware.openfire.interceptor.InterceptorManager; import org.jivesoftware.openfire.interceptor.PacketInterceptor; import org.jivesoftware.openfire.roster.Roster; import org.jivesoftware.openfire.roster.*; import org.jivesoftware.openfire.session.ClientSession; import org.jivesoftware.openfire.session.Session; import org.jivesoftware.openfire.user.User; import org.jivesoftware.openfire.user.UserAlreadyExistsException; import org.jivesoftware.openfire.user.UserNotFoundException; import org.jivesoftware.openfire.vcard.VCardListener; import org.jivesoftware.openfire.vcard.VCardEventDispatcher; import org.jivesoftware.util.Base64; import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.LocaleUtils; import org.jivesoftware.util.NotFoundException; import org.jivesoftware.util.StringUtils; import org.jivesoftware.util.cache.CacheFactory; import org.xmpp.component.Component; import org.xmpp.component.ComponentManager; import org.xmpp.packet.*; import org.xmpp.packet.PacketError.Condition; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.*; import java.util.concurrent.locks.Lock; /** * Base class of all transport implementations. * * Handles all transport non-specific tasks and provides the glue that holds * together server interactions and the legacy service. Does the bulk of * the XMPP related work. Also note that this represents the transport * itself, not an individual session with the transport. * * @author Daniel Henninger */ public abstract class BaseTransport<B extends TransportBuddy> implements Component, RosterEventListener, UserEventListener, PacketInterceptor, SessionEventListener, VCardListener, ChatStateEventListener { static Logger Log = Logger.getLogger(BaseTransport.class); private final ChatStateEventSource chatStateEventSource = new ChatStateEventSource(this); /** * Create a new BaseTransport instance. */ public BaseTransport() { // We've got nothing to do here. } /** * Set up the transport instance. * * @param type Type of the transport. * @param description Description of the transport (for Disco). * @param sessionRouter The session router. */ public void setup(TransportType type, String description, TransportSessionRouter sessionRouter) { // Allow XMPP setting to be overridden. // TODO: Make this more generic. if (type.equals(TransportType.xmpp) && JiveGlobals.getProperty("plugin.gateway.xmpp.overridename") != null) { description = JiveGlobals.getProperty("plugin.gateway.xmpp.overridename"); } this.description = description; this.transportType = type; this.sessionRouter = sessionRouter; permissionManager = new PermissionManager(transportType); } /** * Handles initialization of the transport. */ public void initialize(JID jid, ComponentManager componentManager) { this.jid = jid; this.componentManager = componentManager; rosterManager = XMPPServer.getInstance().getRosterManager(); } /** * Manages all active sessions. * @see net.sf.kraken.session.TransportSessionManager */ public final TransportSessionManager<B> sessionManager = new TransportSessionManager<B>(this); /** * Manages permission information. * @see net.sf.kraken.permissions.PermissionManager */ public PermissionManager permissionManager = null; /** * Manages session routing. * @see net.sf.kraken.session.cluster.TransportSessionRouter */ public TransportSessionRouter sessionRouter = null; /** * JID of the transport in question. */ public JID jid = null; /** * Description of the transport in question. */ public String description = null; /** * Component Manager associated with transport. */ public ComponentManager componentManager = null; /** * Manager component for user rosters. */ public RosterManager rosterManager; /** * MUC transport handler we are associated with. null means no handler. */ public BaseMUCTransport<B> mucTransport; /** * Type of the transport in question. * @see net.sf.kraken.type.TransportType */ public TransportType transportType = null; /** * Handles all incoming XMPP stanzas, passing them to individual * packet type handlers. * * @param packet The packet to be processed. */ public void processPacket(Packet packet) { JID from = packet.getFrom(); JID to = packet.getTo(); if (to == null) { // Well clearly it's for us or it wouldn't have gotten here... packet.setTo(getJID()); to = getJID(); } if (from != null && to != null) { Lock l = CacheFactory.getLock(from.toBareJID()+"@"+transportType.toString()+"pp", sessionRouter.sessionLocations); try { l.lock(); byte[] targetNodeID = sessionRouter.getSession(transportType.toString(), from.toBareJID()); if (targetNodeID != null && !Arrays.equals(targetNodeID, XMPPServer.getInstance().getNodeID().toByteArray())) { RemotePacketRouter router = XMPPServer.getInstance().getRoutingTable().getRemotePacketRouter(); if (router != null) { // Not for our node, send it elsewhere. router.routePacket(targetNodeID, to, packet); return; } } } finally { l.unlock(); } } try { List<Packet> reply = new ArrayList<Packet>(); if (packet instanceof IQ) { reply.addAll(processPacket((IQ)packet)); } else if (packet instanceof Presence) { reply.addAll(processPacket((Presence)packet)); } else if (packet instanceof Message) { reply.addAll(processPacket((Message)packet)); } else { Log.debug("Received an unhandled packet: " + packet.toString()); } if (reply.size() > 0) { for (Packet p : reply) { this.sendPacket(p); } } } catch (Exception e) { Log.warn("Error occured while processing packet:", e); } } /** * Handles all incoming message stanzas. * * @param packet The message packet to be processed. * @return list of packets that will be sent back to the message sender. */ private List<Packet> processPacket(Message packet) { Log.debug("Received message packet: "+packet.toXML()); List<Packet> reply = new ArrayList<Packet>(); JID from = packet.getFrom(); JID to = packet.getTo(); if (to == null) { // Well clearly it's for us or it wouldn't have gotten here... packet.setTo(getJID()); to = getJID(); } try { TransportSession<B> session = sessionManager.getSession(from); if (!session.isLoggedIn()) { Message m = new Message(); m.setError(Condition.service_unavailable); m.setTo(from); m.setFrom(getJID()); m.setBody(LocaleUtils.getLocalizedString("gateway.base.notloggedin", "kraken", Arrays.asList(transportType.toString().toUpperCase()))); reply.add(m); } else if (to.getNode() == null) { // Message to gateway itself. if (packet.getBody() != null) { Message m = new Message(); m.setTo(from); m.setFrom(getJID()); m.setBody(LocaleUtils.getLocalizedString("gateway.base.msgtotransport", "kraken")); reply.add(m); } } else { if (packet.getBody() != null) { if (packet.getChildElement("buzz", NameSpace.SPARKNS) != null) { session.sendBuzzNotification(to, packet.getBody()); } else if (packet.getChildElement("attention", NameSpace.ATTENTIONNS) != null) { session.sendBuzzNotification(to, packet.getBody()); } else { session.sendMessage(to, packet.getBody()); } } else { // Checking for XEP-0022 message events Element eEvent = packet.getChildElement("x", NameSpace.XEVENT); if (eEvent != null && JiveGlobals.getBooleanProperty("plugin.gateway.globsl.messageeventing", true)) { if (eEvent.element("composing") != null) { session.sendChatState(to, ChatStateType.composing); } else { session.sendChatState(to, ChatStateType.paused); } } else { // Ok then, lets check for XEP-0085 chat states if (packet.getChildElement("composing", NameSpace.CHATSTATES) != null) { session.sendChatState(to, ChatStateType.composing); } else if (packet.getChildElement("active", NameSpace.CHATSTATES) != null) { session.sendChatState(to, ChatStateType.active); } else if (packet.getChildElement("inactive", NameSpace.CHATSTATES) != null) { session.sendChatState(to, ChatStateType.inactive); } else if (packet.getChildElement("paused", NameSpace.CHATSTATES) != null) { session.sendChatState(to, ChatStateType.paused); } else if (packet.getChildElement("gone", NameSpace.CHATSTATES) != null) { session.sendChatState(to, ChatStateType.gone); } } if (packet.getChildElement("buzz", NameSpace.SPARKNS) != null) { session.sendBuzzNotification(to, null); } } } } catch (NotFoundException e) { Log.debug("Unable to find session."); Message m = new Message(); m.setError(Condition.service_unavailable); m.setTo(from); m.setFrom(getJID()); m.setBody(LocaleUtils.getLocalizedString("gateway.base.notloggedin", "kraken", Arrays.asList(transportType.toString().toUpperCase()))); reply.add(m); } return reply; } /** * Handles all incoming presence stanzas. * * @param packet The presence packet to be processed. * @return list of packets that will be sent back to the presence requester. */ private List<Packet> processPacket(Presence packet) { Log.debug("Received presence packet: "+packet.toXML()); List<Packet> reply = new ArrayList<Packet>(); JID from = packet.getFrom(); JID to = packet.getTo(); if (to == null) { // Well clearly it's for us or it wouldn't have gotten here... packet.setTo(getJID()); to = getJID(); } if (packet.getType() == Presence.Type.error) { // We don't want to do anything with this. Ignore it. return reply; } try { if (to.getNode() == null) { Collection<Registration> registrations = RegistrationManager.getInstance().getRegistrations(from, this.transportType); if (registrations.isEmpty()) { // User is not registered with us. Log.debug("Unable to find registration."); Presence p = new Presence(); p.setTo(from); p.setFrom(to); p.setError(PacketError.Condition.forbidden); p.setType(Presence.Type.unavailable); reply.add(p); return reply; } if (JiveGlobals.getBooleanProperty("plugin.gateway."+getType()+".registrationstrict", false) && !permissionManager.hasAccess(from)) { Log.debug("Attempt to log in by restricted account: "+from); Presence p = new Presence(); p.setTo(from); p.setFrom(to); p.setError(PacketError.Condition.forbidden); p.setType(Presence.Type.unavailable); reply.add(p); return reply; } Registration registration = registrations.iterator().next(); // This packet is to the transport itself. if (packet.getType() == null) { // A user's resource has come online. TransportSession<B> session; Lock l = CacheFactory.getLock(registration.getJID()+"@"+transportType.toString()+"ns", sessionRouter.sessionLocations); try { l.lock(); session = sessionManager.getSession(from); if (session.hasResource(from.getResource())) { Log.debug("An existing resource has changed status: " + from); if (session.getPriority(from.getResource()) != packet.getPriority()) { session.updatePriority(from.getResource(), packet.getPriority()); } if (session.isHighestPriority(from.getResource())) { // Well, this could represent a status change. session.updateStatus(getPresenceType(packet), packet.getStatus()); } } else { Log.debug("A new resource has come online: " + from); // This is a new resource, lets send them what we know. session.addResource(from.getResource(), packet.getPriority()); // Tell the new resource what the state of their buddy list is. session.getBuddyManager().sendAllAvailablePresences(from); // If this priority is the highest, treat it's status as golden if (session.isHighestPriority(from.getResource())) { session.updateStatus(getPresenceType(packet), packet.getStatus()); } } // Attach the session session.attachSession(); } catch (NotFoundException e) { Log.debug("A new session has come online: " + from); session = this.registrationLoggedIn(registration, from, getPresenceType(packet), packet.getStatus(), packet.getPriority()); sessionManager.storeSession(from, session); } finally { l.unlock(); } } else if (packet.getType() == Presence.Type.unavailable) { // A user's resource has gone offline. TransportSession<B> session; try { session = sessionManager.getSession(from); String resource = from.getResource(); if (session.hasResource(resource)) { if (session.getResourceCount() > 1) { // Just one of the resources, lets adjust accordingly. if (session.isHighestPriority(resource)) { Log.debug("A high priority resource (of multiple) has gone offline: " + from); // Ooh, the highest resource went offline, drop to next highest. session.removeResource(resource); // Lets ask the next highest resource what it's presence is. Presence p = new Presence(Presence.Type.probe); p.setTo(session.getJIDWithHighestPriority()); p.setFrom(this.getJID()); sendPacket(p); } else { Log.debug("A low priority resource (of multiple) has gone offline: " + from); // Meh, lower priority, big whoop. session.removeResource(resource); } } else { Log.debug("A final resource has gone offline: " + from); // No more resources, byebye. this.registrationLoggedOut(session); sessionManager.removeSession(from); } } } catch (NotFoundException e) { Log.debug("Ignoring unavailable presence for inactive seession."); } } else if (packet.getType() == Presence.Type.probe) { // Client is asking for presence status. TransportSession<B> session; try { session = sessionManager.getSession(from); session.sendPresence(from); } catch (NotFoundException e) { Log.debug("Ignoring probe presence for inactive session."); } } else { Log.debug("Ignoring this packet:" + packet.toString()); // Anything else we will ignore for now. } } else { // This packet is to a user at the transport. try { TransportSession<B> session = sessionManager.getSession(from); if (packet.getType() == Presence.Type.probe) { // Presence probe, lets try to answer appropriately. if (session.isLoggedIn()) { try { TransportBuddy buddy = session.getBuddyManager().getBuddy(to); buddy.sendPresence(from); } catch (NotFoundException e) { // User was not found so send an error presence Presence p = new Presence(); p.setTo(from); p.setFrom(to); // TODO: this causes some ugliness in some clients // p.setError(PacketError.Condition.forbidden); // If the user tries to check on a buddy before we are totally logged in // and have the full list, this gets thrown for legit contacts. // We'll send unavailable for now. p.setType(Presence.Type.unavailable); sendPacket(p); } } } else if (packet.getType() == Presence.Type.subscribe) { // For the time being, we are going to lie to the end user that the subscription has worked. Presence p = new Presence(); p.setType(Presence.Type.subscribed); p.setTo(from); p.setFrom(to); sendPacket(p); } else if (packet.getType() == Presence.Type.unsubscribe) { // For the time being, we are going to lie to the end user that the unsubscription has worked. Presence p = new Presence(); p.setType(Presence.Type.unsubscribed); p.setTo(from); p.setFrom(to); sendPacket(p); } else if (packet.getType() == Presence.Type.subscribed) { // let the legacy domain know that the contact was accepted. session.acceptAddContact(packet.getTo()); } else { // Anything else we will ignore for now. } } catch (NotFoundException e) { // Well we just don't care then. Log.debug("User not found while processing " + "presence stanza: " + packet.toXML(), e); } } } catch (Exception e) { Log.debug("Exception while processing packet: ", e); } return reply; } /** * Handles all incoming iq stanzas. * * @param packet The iq packet to be processed. * @return list of packets that will be sent back to the IQ requester. */ private List<Packet> processPacket(IQ packet) { Log.debug("Received iq packet: "+packet.toXML()); List<Packet> reply = new ArrayList<Packet>(); if (packet.getType() == IQ.Type.error) { // Lets not start a loop. Ignore. return reply; } String xmlns = null; Element child = (packet).getChildElement(); if (child != null) { xmlns = child.getNamespaceURI(); } if (xmlns == null) { // No namespace defined. Log.debug("No XMLNS:" + packet.toString()); IQ error = IQ.createResultIQ(packet); error.setError(Condition.bad_request); reply.add(error); return reply; } if (xmlns.equals(NameSpace.DISCO_INFO)) { reply.addAll(handleDiscoInfo(packet)); } else if (xmlns.equals(NameSpace.DISCO_ITEMS)) { reply.addAll(handleDiscoItems(packet)); } else if (xmlns.equals(NameSpace.IQ_GATEWAY)) { reply.addAll(handleIQGateway(packet)); } else if (xmlns.equals(NameSpace.IQ_REGISTER)) { // TODO if all handling is going to be offloaded to // ChannelHandler-like constructs, the exception handling // could/should be made more generic. try { // note that this handler does not make use of the reply-queue. // Instead, it sends packets directly. new RegistrationHandler(this).process(packet); } catch (UnauthorizedException ex) { final IQ result = IQ.createResultIQ(packet); result.setError(Condition.forbidden); reply.add(result); final Message em = new Message(); em.setType(Message.Type.error); em.setTo(packet.getFrom()); em.setFrom(packet.getTo()); em.setBody(ex.getMessage()); reply.add(em); } } else if (xmlns.equals(NameSpace.IQ_VERSION)) { reply.addAll(handleIQVersion(packet)); } else if (xmlns.equals(NameSpace.VCARD_TEMP) && child.getName().equals("vCard")) { reply.addAll(handleVCardTemp(packet)); } else if (xmlns.equals(NameSpace.IQ_ROSTER)) { // No reason to 'argue' about this one. Return success. reply.add(IQ.createResultIQ(packet)); } else if (xmlns.equals(NameSpace.IQ_LAST)) { reply.addAll(handleIQLast(packet)); } else { Log.debug("Unable to handle iq request: " + xmlns); IQ error = IQ.createResultIQ(packet); error.setError(Condition.service_unavailable); reply.add(error); } return reply; } /** * Handle service discovery info request. * * @param packet An IQ packet in the disco info namespace. * @return A list of IQ packets to be returned to the user. */ private List<Packet> handleDiscoInfo(IQ packet) { // TODO: why return a list? we're sure to return always exactly one result. List<Packet> reply = new ArrayList<Packet>(); JID from = packet.getFrom(); IQ result = IQ.createResultIQ(packet); if (packet.getTo().getNode() == null) { // Requested info from transport itself. if (from.getNode() == null || RegistrationManager.getInstance().isRegistered(from, this.transportType) || permissionManager.hasAccess(from)) { Element response = DocumentHelper.createElement(QName.get("query", NameSpace.DISCO_INFO)); response.addElement("identity") .addAttribute("category", "gateway") .addAttribute("type", this.transportType.discoIdentity()) .addAttribute("name", this.description); response.addElement("feature") .addAttribute("var", NameSpace.DISCO_INFO); response.addElement("feature") .addAttribute("var", NameSpace.DISCO_ITEMS); response.addElement("feature") .addAttribute("var", NameSpace.IQ_GATEWAY); response.addElement("feature") .addAttribute("var", NameSpace.IQ_REGISTER); response.addElement("feature") .addAttribute("var", NameSpace.IQ_VERSION); response.addElement("feature") .addAttribute("var", NameSpace.IQ_LAST); response.addElement("feature") .addAttribute("var", NameSpace.VCARD_TEMP); if (RegistrationManager.getInstance().isRegistered(from, this.transportType)) { response.addElement("feature") .addAttribute("var", NameSpace.IQ_REGISTERED); } result.setChildElement(response); } else { result.setError(Condition.forbidden); } } else { // Requested info from a gateway user. final TransportSession<B> session; try { session = sessionManager.getSession(packet.getFrom()); if ((from.getNode() == null || permissionManager.hasAccess(from)) && session != null) { final Element response = DocumentHelper.createElement(QName.get("query", NameSpace.DISCO_INFO)); response.addElement("identity") .addAttribute("category", "client") .addAttribute("type", "pc"); response.addElement("feature") .addAttribute("var", NameSpace.DISCO_INFO); for(final SupportedFeature feature : session.supportedFeatures) { response.addElement("feature") .addAttribute("var", feature.getVar()); } result.setChildElement(response); } else { result.setError(Condition.forbidden); } } catch (NotFoundException ex) { result.setError(Condition.item_not_found); } } reply.add(result); return reply; } /** * Handle service discovery items request. * * @param packet An IQ packet in the disco items namespace. * @return A list of IQ packets to be returned to the user. */ private List<Packet> handleDiscoItems(IQ packet) { List<Packet> reply = new ArrayList<Packet>(); IQ result = IQ.createResultIQ(packet); Element response = DocumentHelper.createElement(QName.get("query", NameSpace.DISCO_ITEMS)); result.setChildElement(response); reply.add(result); return reply; } /** * Handle gateway translation service request. * * @param packet An IQ packet in the iq gateway namespace. * @return A list of IQ packets to be returned to the user. */ private List<Packet> handleIQGateway(IQ packet) { List<Packet> reply = new ArrayList<Packet>(); if (packet.getType() == IQ.Type.get) { IQ result = IQ.createResultIQ(packet); Element query = DocumentHelper.createElement(QName.get("query", NameSpace.IQ_GATEWAY)); query.addElement("desc").addText(LocaleUtils.getLocalizedString("gateway.base.enterusername", "kraken", Arrays.asList(transportType.toString().toUpperCase()))); query.addElement("prompt"); result.setChildElement(query); reply.add(result); } else if (packet.getType() == IQ.Type.set) { IQ result = IQ.createResultIQ(packet); String prompt = null; Element promptEl = packet.getChildElement().element("prompt"); if (promptEl != null) { prompt = promptEl.getTextTrim(); } if (prompt == null) { result.setError(Condition.bad_request); } else { JID jid = this.convertIDToJID(prompt); Element query = DocumentHelper.createElement(QName.get("query", NameSpace.IQ_GATEWAY)); // This is what Psi expects query.addElement("prompt").addText(jid.toString()); // This is JEP complient query.addElement("jid").addText(jid.toString()); result.setChildElement(query); } reply.add(result); } return reply; } /** * Handle vcard request. * * @param packet An IQ packet in the vcard-temp namespace. * @return A list of IQ packets to be returned to the user. */ private List<Packet> handleVCardTemp(IQ packet) { List<Packet> reply = new ArrayList<Packet>(); JID from = packet.getFrom(); JID to = packet.getTo(); if (packet.getType() == IQ.Type.get) { IQ result = IQ.createResultIQ(packet); if (from.getNode() != null) { try { TransportSession<B> session = sessionManager.getSession(from); Element vcard = session.getBuddyManager().getBuddy(to).getVCard(); result.setChildElement(vcard); } catch (NotFoundException e) { Log.debug("Contact not found while retrieving vcard for: "+from); result.setError(Condition.item_not_found); } } else { result.setError(Condition.feature_not_implemented); } reply.add(result); } else if (packet.getType() == IQ.Type.set) { IQ result = IQ.createResultIQ(packet); result.setError(Condition.forbidden); reply.add(result); } return reply; } /** * Handle last request. * * @param packet An IQ packet in the jabber:iq:last namespace. * @return A list of IQ packets to be returned to the user. */ private List<Packet> handleIQLast(IQ packet) { List<Packet> reply = new ArrayList<Packet>(); JID from = packet.getFrom(); JID to = packet.getTo(); if (packet.getType() == IQ.Type.get) { IQ result = IQ.createResultIQ(packet); if (from.getNode() != null) { try { TransportSession<B> session = sessionManager.getSession(from); Element response = DocumentHelper.createElement(QName.get("query", NameSpace.IQ_LAST)); Long timestamp = session.getBuddyManager().getBuddy(to).getLastActivityTimestamp(); String lastevent = session.getBuddyManager().getBuddy(to).getLastActivityEvent(); response.addAttribute("seconds", new Long(new Date().getTime() - timestamp).toString()); if (lastevent != null) { response.addCDATA(lastevent); } result.setChildElement(response); } catch (NotFoundException e) { Log.debug("Contact not found while retrieving last activity for: "+from); result.setError(Condition.item_not_found); } } else { result.setError(Condition.feature_not_implemented); } reply.add(result); } else if (packet.getType() == IQ.Type.set) { IQ result = IQ.createResultIQ(packet); result.setError(Condition.forbidden); reply.add(result); } return reply; } /** * Handle version request. * * @param packet An IQ packet in the iq version namespace. * @return A list of IQ packets to be returned to the user. */ private List<Packet> handleIQVersion(IQ packet) { List<Packet> reply = new ArrayList<Packet>(); if (packet.getType() == IQ.Type.get) { IQ result = IQ.createResultIQ(packet); Element query = DocumentHelper.createElement(QName.get("query", NameSpace.IQ_VERSION)); query.addElement("name").addText("Openfire " + this.getDescription()); query.addElement("version").addText(XMPPServer.getInstance().getServerInfo().getVersion().getVersionString() + " - " + this.getVersionString()); query.addElement("os").addText(System.getProperty("os.name")); result.setChildElement(query); reply.add(result); } return reply; } /** * Converts a legacy username to a JID. * * @param username Username to be converted to a JID. * @return The legacy username as a JID. */ public JID convertIDToJID(String username) { if (username.indexOf("/") > -1) { username = username.substring(0, username.indexOf("/")); } if (JiveGlobals.getBooleanProperty("plugin.gateway.tweak.percenthack", false)) { return new JID(username.replace('@', '%').replace(" ", ""), this.jid.getDomain(), null); } else { return new JID(JID.escapeNode(username.replace(" ", "")), this.jid.getDomain(), null); } } /** * Converts a JID to a legacy username. * * @param jid JID to be converted to a legacy username. * @return THe legacy username as a String. */ public String convertJIDToID(JID jid) { if (JiveGlobals.getBooleanProperty("plugin.gateway.tweak.percenthack", false)) { return jid.getNode().replace('%', '@'); } else { return JID.unescapeNode(jid.getNode()); } } /** * Sets up a presence packet according to a presenceType setting. * * @param packet packet to set up. * @param presenceType presence type to set up. */ public void setUpPresencePacket(Presence packet, PresenceType presenceType) { if (presenceType.equals(PresenceType.away)) { packet.setShow(Presence.Show.away); } else if (presenceType.equals(PresenceType.xa)) { packet.setShow(Presence.Show.xa); } else if (presenceType.equals(PresenceType.dnd)) { packet.setShow(Presence.Show.dnd); } else if (presenceType.equals(PresenceType.chat)) { packet.setShow(Presence.Show.chat); } else if (presenceType.equals(PresenceType.unavailable)) { packet.setType(Presence.Type.unavailable); } else if (presenceType.equals(PresenceType.unknown)) { // We consider this unavailable since we don't know what it is. packet.setType(Presence.Type.unavailable); } } /** * Gets an easy to use presence type from a presence packet. * * @param packet A presence packet from which the type will be pulled. * @return the presence type of the specified packet. */ public PresenceType getPresenceType(Presence packet) { Presence.Type ptype = packet.getType(); Presence.Show stype = packet.getShow(); if (stype == Presence.Show.chat) { return PresenceType.chat; } else if (stype == Presence.Show.away) { return PresenceType.away; } else if (stype == Presence.Show.xa) { return PresenceType.xa; } else if (stype == Presence.Show.dnd) { return PresenceType.dnd; } else if (ptype == Presence.Type.unavailable) { return PresenceType.unavailable; } else if (packet.isAvailable()) { return PresenceType.available; } else { return PresenceType.unknown; } } /** * Handles startup of the transport. */ public void start() { RosterEventDispatcher.addListener(this); UserEventDispatcher.addListener(this); SessionEventDispatcher.addListener(this); VCardEventDispatcher.addListener(this); InterceptorManager.getInstance().addInterceptor(this); if (!JiveGlobals.getBooleanProperty("plugin.gateway.tweak.noprobeonstart", false)) { // Probe all registered users [if they are logged in] to auto-log them in // TODO: Do we need to account for local vs other node sessions? for (ClientSession session : SessionManager.getInstance().getSessions()) { try { JID jid = XMPPServer.getInstance().createJID(session.getUsername(), null); if (RegistrationManager.getInstance().isRegistered(jid, getType())) { Presence p = new Presence(Presence.Type.probe); p.setFrom(this.getJID()); p.setTo(jid); sendPacket(p); } } catch (UserNotFoundException e) { // Not a valid user for the gateway then } } } } /** * Handles shutdown of the transport. * * Cleans up all active sessions. */ public void shutdown() { InterceptorManager.getInstance().removeInterceptor(this); VCardEventDispatcher.removeListener(this); SessionEventDispatcher.removeListener(this); RosterEventDispatcher.removeListener(this); UserEventDispatcher.removeListener(this); // Disconnect everyone's session for (TransportSession<B> session : sessionManager.getSessions()) { registrationLoggedOut(session); session.removeAllResources(); } sessionManager.shutdown(); } /** * Returns the jid of the transport. * * @return the jid of the transport. */ public JID getJID() { return this.jid; } /** * Returns the roster manager for the transport. * * @return the roster manager for the transport. */ public RosterManager getRosterManager() { return this.rosterManager; } /** * @return the name (type) of the transport. */ public String getName() { return transportType.toString(); } /** * @return the MUC transport we are associated with. */ public BaseMUCTransport<B> getMUCTransport() { return mucTransport; } /** * @return the type of the transport. */ public TransportType getType() { return transportType; } /** * @return the description of the transport. */ public String getDescription() { return description; } /** * @return the component manager of the transport. */ public ComponentManager getComponentManager() { return componentManager; } /** * @return the session manager of the transport. */ public TransportSessionManager<B> getSessionManager() { return sessionManager; } /** * @return the session router of the transport. */ public TransportSessionRouter getSessionRouter() { return sessionRouter; } /** * @return the chat state managing object of the transport */ public ChatStateEventSource getChatStateEventSource() { return chatStateEventSource; } /** * Retains the version string for later requests. */ private String versionString = null; /** * @return the version string of the gateway. */ public String getVersionString() { if (versionString == null) { PluginManager pluginManager = XMPPServer.getInstance().getPluginManager(); versionString = pluginManager.getVersion(pluginManager.getPlugin("kraken")); } return versionString; } /** * Either updates or adds a JID to a user's roster. * * Tries to only edit the roster if it has to. * * @param userjid JID of user to have item added to their roster. * @param contactjid JID to add to roster. * @param nickname Nickname of item. (can be null) * @param groups List of group the item is to be placed in. (can be null) * @throws UserNotFoundException if userjid not found. */ public void addOrUpdateRosterItem(JID userjid, JID contactjid, String nickname, Collection<String> groups) throws UserNotFoundException { // SubTypes "FROM" and "BOTH" should be avoided for legacy user contacts. Instead, use "TO". The gateway // component itself does have a "BOTH" subscription. This cuts down on needless traffic: the gateway component // will broadcast presence changes to every legacy contact - there's no need for the local domain to // address each and every legacy contact independently. RosterItem.SubType subType = (contactjid.getNode() == null ? RosterItem.SUB_BOTH : RosterItem.SUB_TO); RosterItem.AskType askType = RosterItem.ASK_NONE; addOrUpdateRosterItem(userjid, contactjid, nickname, groups, subType, askType); } /** * Either updates or adds a JID to a user's roster. * * Tries to only edit the roster if it has to. * * @param userjid JID of user to have item added to their roster. * @param contactjid JID to add to roster. * @param nickname Nickname of item. (can be null) * @param groups List of group the item is to be placed in. (can be null) * @param subtype Specific subscription setting. * @param asktype Specific ask setting. * @throws UserNotFoundException if userjid not found. */ public void addOrUpdateRosterItem(JID userjid, JID contactjid, String nickname, Collection<String> groups, RosterItem.SubType subtype, RosterItem.AskType asktype) throws UserNotFoundException { Log.debug("add or update roster item " + contactjid + " for: " + userjid); try { final Roster roster = rosterManager.getRoster(userjid.getNode()); try { RosterItem gwitem = roster.getRosterItem(contactjid); Log.debug("Found existing roster item " + contactjid + " for: " + userjid + ". We will update if required."); boolean changed = false; if (gwitem.getSubStatus() != subtype) { gwitem.setSubStatus(subtype); changed = true; } if (gwitem.getAskStatus() != asktype) { gwitem.setAskStatus(asktype); changed = true; } // This could probably be simplified, for now I'm going with brute force logic. // gnickname is null, nickname is null, leave // gnickname is not null, nickname is null, set gnickname to null // gnickname is null, nickname is not null, set gnickname to nickname // gnickname is not null, nickname is not null, if different, set gnickname to nickname if ( (gwitem.getNickname() != null && nickname == null) || (gwitem.getNickname() == null && nickname != null) || (gwitem.getNickname() != null && nickname != null && !gwitem.getNickname().equals(nickname))) { gwitem.setNickname(nickname); changed = true; } List<String> curgroups = gwitem.getGroups(); // This could probably be simplified, for now I'm going with brute force logic. // curgroups is null, groups is null, leave // curgroups is not null and has entries, groups is null or empty, set curgroups to empty // curgroups is null or empty, groups is not null and has entries, set curgroups to groups // curgroups is not null, groups is not null, if their sizes are different or curgroups does not contain all of groups, set curgroups to groups if ( ((curgroups != null && curgroups.size() > 0) && (groups == null || groups.size() == 0)) || ((curgroups == null || curgroups.size() == 0) && (groups != null && groups.size() > 0)) || (curgroups != null && groups != null && ((curgroups.size() != groups.size()) || !curgroups.containsAll(groups)))) { try { gwitem.setGroups((List<String>)(groups != null ? groups : new ArrayList<String>())); changed = true; } catch (Exception ee) { Log.debug("Exception while setting groups for roster item:", ee); } } if (changed) { Log.debug("Updating existing roster item " + contactjid + " for: " + userjid); roster.updateRosterItem(gwitem); } else { Log.debug("Update of existing roster item " + contactjid + " for: " + userjid + " can be skipped - nothing changed."); } } catch (UserNotFoundException e) { try { // Create new roster item for the gateway service or legacy contact. Only // roster items related to the gateway service will be persistent. Roster // items of legacy users are never persisted in the DB. (unless tweak enabled) Log.debug("Creating new roster item " + contactjid + " for: " + userjid + ". No existing item was found."); final RosterItem gwitem = roster.createRosterItem(contactjid, false, contactjid.getNode() == null || JiveGlobals.getBooleanProperty("plugin.gateway.tweak.persistentroster", false)); gwitem.setSubStatus(subtype); gwitem.setAskStatus(asktype); gwitem.setNickname(nickname); try { gwitem.setGroups((List<String>)groups); } catch (Exception ee) { Log.debug("Exception while setting groups for gateway item:", ee); } roster.updateRosterItem(gwitem); } catch (UserAlreadyExistsException ee) { Log.debug("getRosterItem claims user exists, but couldn't find via getRosterItem?", ee); } catch (Exception ee) { Log.debug("Exception while creating roster item:", ee); } } } catch (UserNotFoundException e) { throw new UserNotFoundException("Could not find roster for " + userjid.toString()); } } /** * Either updates or adds a JID to a user's roster. * * Tries to only edit the roster if it has to. * * @param userjid JID of user to have item added to their roster. * @param contactjid JID to add to roster. * @param nickname Nickname of item. (can be null) * @param group Group item is to be placed in. (can be null) * @throws UserNotFoundException if userjid not found. */ public void addOrUpdateRosterItem(JID userjid, JID contactjid, String nickname, String group) throws UserNotFoundException { ArrayList<String> groups = null; if (group != null) { groups = new ArrayList<String>(); groups.add(group); } addOrUpdateRosterItem(userjid, contactjid, nickname, groups); } /** * Either updates or adds a JID to a user's roster. * * Tries to only edit the roster if it has to. * * @param userjid JID of user to have item added to their roster. * @param contactjid JID to add to roster. * @param nickname Nickname of item. (can be null) * @param group Group item is to be placed in. (can be null) * @param subtype Specific subscription setting. * @param asktype Specific ask setting. * @throws UserNotFoundException if userjid not found. */ public void addOrUpdateRosterItem(JID userjid, JID contactjid, String nickname, String group, RosterItem.SubType subtype, RosterItem.AskType asktype) throws UserNotFoundException { ArrayList<String> groups = null; if (group != null) { groups = new ArrayList<String>(); groups.add(group); } addOrUpdateRosterItem(userjid, contactjid, nickname, groups, subtype, asktype); } /** * Either updates or adds a contact to a user's roster. * * @param userjid JID of user to have item added to their roster. * @param contactid String contact name, will be translated to JID. * @param nickname Nickname of item. (can be null) * @param group Group item is to be placed in. (can be null) * @throws UserNotFoundException if userjid not found. */ public void addOrUpdateRosterItem(JID userjid, String contactid, String nickname, String group) throws UserNotFoundException { addOrUpdateRosterItem(userjid, convertIDToJID(contactid), nickname, group); } /** * Either updates or adds a contact to a user's roster. * * @param userjid JID of user to have item added to their roster. * @param contactid String contact name, will be translated to JID. * @param nickname Nickname of item. (can be null) * @param groups Group items is to be placed in. (can be null) * @throws UserNotFoundException if userjid not found. */ public void addOrUpdateRosterItem(JID userjid, String contactid, String nickname, Collection<String> groups) throws UserNotFoundException { addOrUpdateRosterItem(userjid, convertIDToJID(contactid), nickname, groups); } /** * Either updates or adds a contact to a user's roster. * * @param userjid JID of user to have item added to their roster. * @param contactid String contact name, will be translated to JID. * @param nickname Nickname of item. (can be null) * @param group Group item is to be placed in. (can be null) * @param subtype Specific subscription setting. * @param asktype Specific ask setting. * @throws UserNotFoundException if userjid not found. */ public void addOrUpdateRosterItem(JID userjid, String contactid, String nickname, String group, RosterItem.SubType subtype, RosterItem.AskType asktype) throws UserNotFoundException { addOrUpdateRosterItem(userjid, convertIDToJID(contactid), nickname, group, subtype, asktype); } /** * Either updates or adds a contact to a user's roster. * * @param userjid JID of user to have item added to their roster. * @param contactid String contact name, will be translated to JID. * @param nickname Nickname of item. (can be null) * @param groups Group items is to be placed in. (can be null) * @param subtype Specific subscription setting. * @param asktype Specific ask setting. * @throws UserNotFoundException if userjid not found. */ public void addOrUpdateRosterItem(JID userjid, String contactid, String nickname, Collection<String> groups, RosterItem.SubType subtype, RosterItem.AskType asktype) throws UserNotFoundException { addOrUpdateRosterItem(userjid, convertIDToJID(contactid), nickname, groups, subtype, asktype); } /** * Removes a roster item from a user's roster. * * @param userjid JID of user whose roster we will interact with. * @param contactjid JID to be removed from roster. * @throws UserNotFoundException if userjid not found. */ public void removeFromRoster(JID userjid, JID contactjid) throws UserNotFoundException { // Clean up the user's contact list. try { Roster roster = rosterManager.getRoster(userjid.getNode()); for (RosterItem ri : roster.getRosterItems()) { if (ri.getJid().toBareJID().equals(contactjid.toBareJID())) { try { roster.deleteRosterItem(ri.getJid(), false); } catch (Exception e) { Log.debug("Error removing roster item: " + ri.toString(), e); } } } } catch (UserNotFoundException e) { throw new UserNotFoundException("Could not find roster for " + userjid.toString()); } } /** * Removes a roster item from a user's roster based on a legacy contact. * * @param userjid JID of user whose roster we will interact with. * @param contactid Contact to be removed, will be translated to JID. * @throws UserNotFoundException if userjid not found. */ void removeFromRoster(JID userjid, String contactid) throws UserNotFoundException { // Clean up the user's contact list. removeFromRoster(userjid, convertIDToJID(contactid)); } /** * Sync a user's roster with their legacy contact list. * * Given a collection of transport buddies, syncs up the user's * roster by fixing any nicknames, group assignments, adding and removing * roster items, and generally trying to make the jabber roster list * assigned to the transport's JID look at much like the legacy buddy * list as possible. This is a very extensive operation. You do not * want to do this very often. Typically once right after the person * has logged into the legacy service. * * @param userjid JID of user who's roster we are syncing with. * @param legacyitems List of TransportBuddy's to be synced. * @throws UserNotFoundException if userjid not found. */ public void syncLegacyRoster(JID userjid, Collection<B> legacyitems) throws UserNotFoundException { Log.debug("Syncing Legacy Roster: "+legacyitems); try { Roster roster = rosterManager.getRoster(userjid.getNode()); // Lets lock down the roster from update notifications if there's an active session. try { TransportSession<B> session = sessionManager.getSession(userjid.getNode()); session.lockRoster(); } catch (NotFoundException e) { // No active session? Then no problem. } // First thing first, we want to build ourselves an easy mapping. Map<JID,TransportBuddy> legacymap = new HashMap<JID, TransportBuddy>(); for (TransportBuddy buddy : legacyitems) { // Log.debug("ROSTERSYNC: Mapping "+buddy.getName()); legacymap.put(buddy.getJID(), buddy); } // Now, lets go through the roster and see what matches up. for (RosterItem ri : roster.getRosterItems()) { if (!ri.getJid().getDomain().equals(this.jid.getDomain())) { // Not our contact to care about. continue; } if (ri.getJid().getNode() == null) { // This is a transport instance, lets leave it alone. continue; } JID jid = new JID(ri.getJid().toBareJID()); if (legacymap.containsKey(jid)) { Log.debug("ROSTERSYNC: We found, updating " + jid.toString()); // Ok, matched a legacy to jabber roster item // Lets update if there are differences TransportBuddy buddy = legacymap.get(jid); try { if (buddy.getAskType() != null && buddy.getSubType() != null) { this.addOrUpdateRosterItem(userjid, buddy.getName(), buddy.getNickname(), buddy.getGroups(), buddy.getSubType(), buddy.getAskType()); } else { this.addOrUpdateRosterItem(userjid, buddy.getName(), buddy.getNickname(), buddy.getGroups()); } } catch (UserNotFoundException e) { Log.debug("Failed updating roster item", e); } legacymap.remove(jid); } else { Log.debug("ROSTERSYNC: We did not find, removing " + jid.toString()); // This person is apparantly no longer in the legacy roster. try { this.removeFromRoster(userjid, jid); } catch (UserNotFoundException e) { Log.debug("Failed removing roster item", e); } } } // Ok, we should now have only new items from the legacy roster for (TransportBuddy buddy : legacymap.values()) { Log.debug("ROSTERSYNC: We have new, adding " + buddy.getName()); try { this.addOrUpdateRosterItem(userjid, buddy.getName(), buddy.getNickname(), buddy.getGroups()); } catch (UserNotFoundException e) { Log.debug("Failed adding new roster item", e); } } // All done, lets unlock the roster. try { TransportSession<B> session = sessionManager.getSession(userjid.getNode()); session.unlockRoster(); } catch (NotFoundException e) { // No active session? Then no problem. } } catch (UserNotFoundException e) { throw new UserNotFoundException("Could not find roster for " + userjid.toString()); } } /** * Cleans a roster of entries related to this transport. * * This function will run through the roster of the specified user and clean up any * entries that share the domain of this transport. Depending on the removeNonPersistent * option, it will either leave or keep the non-persistent 'contact' entries. * * @param jid JID of the user whose roster we want to clean up. * @param leaveDomain If set, we do not touch the roster item associated with the domain itself. * @param removeNonPersistent If set, we will also remove non-persistent items. * @throws UserNotFoundException if the user is not found. */ public void cleanUpRoster(JID jid, Boolean leaveDomain, Boolean removeNonPersistent) throws UserNotFoundException { try { Roster roster = rosterManager.getRoster(jid.getNode()); // Lets lock down the roster from update notifications if there's an active session. try { TransportSession<B> session = sessionManager.getSession(jid.getNode()); session.lockRoster(); } catch (NotFoundException e) { // No active session? Then no problem. } for (RosterItem ri : roster.getRosterItems()) { if (ri.getJid().getDomain().equals(this.jid.getDomain())) { if (ri.isShared()) { // Is a shared item we can't really touch. continue; } if (!removeNonPersistent && ri.getID() == 0) { // Is a non-persistent roster item. continue; } if (leaveDomain && ri.getJid().getNode() == null) { // The actual transport domain item. continue; } try { Log.debug("Cleaning up roster entry " + ri.getJid().toString()); roster.deleteRosterItem(ri.getJid(), false); } catch (Exception e) { Log.debug("Error removing roster item: " + ri.toString(), e); } } } // All done, lets unlock the roster. try { TransportSession<B> session = sessionManager.getSession(jid.getNode()); session.unlockRoster(); } catch (NotFoundException e) { // No active session? Then no problem. } } catch (UserNotFoundException e) { throw new UserNotFoundException("Unable to find roster."); } } /** * Cleans a roster of entries related to this transport that are not shared. * * This function will run through the roster of the specified user and clean up any * entries that share the domain of this transport. This is primarily used during registration * to clean up leftovers from other transports. * * @param jid JID of user whose roster we want to clean up. * @param leaveDomain If set, we do not touch the roster item associated with the domain itself. * @throws UserNotFoundException if the user is not found. */ public void cleanUpRoster(JID jid, Boolean leaveDomain) throws UserNotFoundException { try { cleanUpRoster(jid, leaveDomain, false); } catch (UserNotFoundException e) { throw new UserNotFoundException("Unable to find roster."); } } /** * Sends offline packets for an entire roster to the target user. * * This function will run through the roster of the specified user and send offline * presence packets for each roster item. This is typically used when a user logs * off so that all of the associated roster items appear offline. This does not send * the unavailable presence for the transport itself. * * @param jid JID of user whose roster we want to clean up. * @throws UserNotFoundException if the user is not found. * @deprecated Use net.sf.kraken.roster.TransportBuddyManager#sendOfflineForAllAvailablePresences(JID) */ @Deprecated public void notifyRosterOffline(JID jid) throws UserNotFoundException { try { Roster roster = rosterManager.getRoster(jid.getNode()); for (RosterItem ri : roster.getRosterItems()) { if (ri.getJid().getNode() != null && ri.getJid().getDomain().equals(this.jid.getDomain())) { Presence p = new Presence(Presence.Type.unavailable); p.setTo(jid); p.setFrom(ri.getJid()); sendPacket(p); } } } catch (UserNotFoundException e) { throw new UserNotFoundException("Unable to find roster."); } } /** * Sends a packet through the component manager as the component. * * @param packet Packet to be sent. */ public void sendPacket(Packet packet) { // Prevent future chat state notifications from being send out for // contacts that are offline. if (packet instanceof Presence) { final Presence presence = (Presence) packet; if (presence.getType() == Presence.Type.unavailable && presence.getFrom().getNode() != null) { this.chatStateEventSource.isGone(presence.getFrom(), presence.getTo()); } } Log.debug(getType().toString()+": Sending packet: "+packet.toXML()); try { this.componentManager.sendPacket(this, packet); } catch (Exception e) { Log.warn("Failed to deliver packet: " + packet.toString()); } } /** * Sends a simple message through he component manager. * * @param to Who the message is for. * @param from Who the message is from. * @param msg Message to be send. * @param type Type of message to be sent. */ public void sendMessage(JID to, JID from, String msg, Message.Type type) { Message m = new Message(); m.setType(type); m.setFrom(from); m.setTo(to); m.setBody(net.sf.kraken.util.StringUtils.removeInvalidXMLCharacters(msg)); if (msg.length() == 0) { Log.debug("Dropping empty message packet."); return; } if (type.equals(Message.Type.chat) || type.equals(Message.Type.normal)) { chatStateEventSource.isActive(from, to); m.addChildElement("active", NameSpace.CHATSTATES); if (JiveGlobals.getBooleanProperty("plugin.gateway.globsl.messageeventing", true)) { Element xEvent = m.addChildElement("x", "jabber:x:event"); // xEvent.addElement("id"); xEvent.addElement("composing"); } } else if (type.equals(Message.Type.error)) { // Error responses require error elements, even if we aren't going to do it "right" yet // TODO: All -real- error handling m.setError(Condition.undefined_condition); } try { TransportSession session = sessionManager.getSession(to); if (session.getDetachTimestamp() != 0) { // This is a detached session then, so lets store the packet instead of delivering. session.storePendingPacket(m); return; } } catch (NotFoundException e) { // No session? That's "fine", allow it through, it's probably something from the transport itself. } sendPacket(m); } /** * Sends a simple message through the component manager. * * @param to Who the message is for. * @param from Who the message is from. * @param msg Message to be send. */ public void sendMessage(JID to, JID from, String msg) { sendMessage(to, from, msg, Message.Type.chat); } public static SimpleDateFormat UTC_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss"); /** * Sends an offline message through the component manager. * * @param to Who the message is for. * @param from Who the message is from. * @param msg Message to be send. * @param type Type of message to be sent. * @param time Time when the message was originally sent. * @param reason Reason for offline message (can be null) */ public void sendOfflineMessage(JID to, JID from, String msg, Message.Type type, Date time, String reason) { Message m = new Message(); m.setType(type); m.setFrom(from); m.setTo(to); m.setBody(net.sf.kraken.util.StringUtils.removeInvalidXMLCharacters(msg)); if (msg.length() == 0) { Log.debug("Dropping empty message packet."); return; } Element delay = m.addChildElement("delay", NameSpace.DELAY); // delay.addAttribute("from", from.toBareJID()); delay.addAttribute("stamp", UTC_FORMAT.format(time)); if (reason != null) { delay.addCDATA(reason); } Element offline = m.addChildElement("offline", NameSpace.OFFLINE); offline.addElement("item").addAttribute("node", UTC_FORMAT.format(time)); Element x = m.addChildElement("x", NameSpace.X_DELAY); // x.addAttribute("from", from.toBareJID()); x.addAttribute("stamp", UTC_FORMAT.format(time)); if (reason != null) { x.addCDATA(reason); } // m.addChildElement("time",""); // m.getChildElement("time","").setText(time); try { TransportSession session = sessionManager.getSession(to); if (session.getDetachTimestamp() != 0) { // This is a detached session then, so lets store the packet instead of delivering. session.storePendingPacket(m); return; } } catch (NotFoundException e) { // No session? That's "fine", allow it through, it's probably something from the transport itself. } sendPacket(m); } /** * Sends an offline message through the component manager. * * @param to Who the message is for. * @param from Who the message is from. * @param msg Message to be send. * @param time Time when the message was originally sent. * @param reason Reason for offline message (can be null) */ public void sendOfflineMessage(JID to, JID from, String msg, Date time, String reason) { sendOfflineMessage(to, from, msg, Message.Type.chat, time, reason); } /** * Sends a buzz notiifcation through the component manager. * * @param to Who the notification is for. * @param from Who the notification is from. * @param message Message attached to buzz notification. */ private void sendBuzzNotification(JID to, JID from, String message) { Message m = new Message(); // m.setType(Message.Type.headline); m.setTo(to); m.setFrom(from); // if (message != null && message.length() > 0) { // m.setBody(message); // } m.addChildElement("buzz", NameSpace.SPARKNS); // m.addChildElement("attention", "http://www.xmpp.org/extensions/xep-0224.html#ns"); sendPacket(m); } /** * Sends an Attention notification, as specified in XEP-0224. * * @param to The entity that of which the attention is being tried to capture. * @param from The entity that is trying to capturing another ones attention. * @param message An optional (headline) message (can be <tt>null</tt>). */ public void sendAttentionNotification(JID to, JID from, String message) { // TODO: query receiving entity for support. // TODO: only buzz clients that support the Spark namespace! sendBuzzNotification(to, from, message); final Message stanza = new Message(); if (message != null && message.trim().length() > 0) { stanza.setBody(message); } stanza.addChildElement("attention", NameSpace.ATTENTIONNS); stanza.setType(Message.Type.headline); stanza.setTo(to); stanza.setFrom(from); sendPacket(stanza); } /* (non-Javadoc) * @see net.sf.kraken.util.chatstate.ChatStateEventListener#chatStateChange(net.sf.kraken.util.chatstate.ChatStateEvent) */ public void chatStateChange(ChatStateChangeEvent event) { if (this.getJID().getDomain().equals(event.sender.getDomain())) { // a local user is receiving a chat state event switch (event.type) { case active: // No need to do anything here. "Active" chat states are // included in each outgoing message. Sending them again // here would only lead to duplicate messages. //sendChatActiveNotification(event.receiver, event.sender); break; case composing: sendComposingNotification(event.receiver, event.sender); break; case gone: sendChatGoneNotification(event.receiver, event.sender); break; case inactive: sendChatInactiveNotification(event.receiver, event.sender); break; case paused: sendComposingPausedNotification(event.receiver, event.sender); break; default: // The code should include cases for every possible state. throw new AssertionError(event.type); } } else if (this.getJID().getDomain().equals(event.receiver.getDomain())) { // a local user is sending an event try { getSessionManager().getSession(event.sender).sendChatState(event.receiver, event.type); } catch (NotFoundException e) { Log.debug("A local user that does not have a session with us is " + "sending chat state messages to alegacy user. Event:" + event); } } else { Log.warn("Cannot send chat state event, as nor the sender or recipient " + "appears to be a locally registered (and online) user. Event: " + event); } } /** * Sends a typing notification through the component manager. * * This will check whether the person supports typing notifications before sending. * TODO: actually check for typing notification support * * @param to Who the notification is for. * @param from Who the notification is from. */ public void sendComposingNotification(JID to, JID from) { Message m = new Message(); m.setType(Message.Type.chat); m.setTo(to); m.setFrom(from); if (JiveGlobals.getBooleanProperty("plugin.gateway.globsl.messageeventing", true)) { Element xEvent = m.addChildElement("x", "jabber:x:event"); xEvent.addElement("id"); xEvent.addElement("composing"); } m.addChildElement("composing", NameSpace.CHATSTATES); sendPacket(m); } /** * Sends a typing paused notification through the component manager. * * This will check whether the person supports typing notifications before sending. * TODO: actually check for typing notification support * * @param to Who the notification is for. * @param from Who the notification is from. */ public void sendComposingPausedNotification(JID to, JID from) { Message m = new Message(); m.setType(Message.Type.chat); m.setTo(to); m.setFrom(from); if (JiveGlobals.getBooleanProperty("plugin.gateway.globsl.messageeventing", true)) { Element xEvent = m.addChildElement("x", "jabber:x:event"); xEvent.addElement("id"); } m.addChildElement("paused", NameSpace.CHATSTATES); sendPacket(m); } /** * Sends an inactive chat session notification through the component manager. * * This will check whether the person supports typing notifications before sending. * TODO: actually check for typing notification support * * @param to Who the notification is for. * @param from Who the notification is from. */ public void sendChatInactiveNotification(JID to, JID from) { Message m = new Message(); m.setType(Message.Type.chat); m.setTo(to); m.setFrom(from); if (JiveGlobals.getBooleanProperty("plugin.gateway.globsl.messageeventing", true)) { Element xEvent = m.addChildElement("x", "jabber:x:event"); xEvent.addElement("id"); } m.addChildElement("inactive", NameSpace.CHATSTATES); sendPacket(m); } /** * Sends a gone chat session notification through the component manager. * * This will check whether the person supports typing notifications before sending. * TODO: actually check for typing notification support * * @param to Who the notification is for. * @param from Who the notification is from. */ public void sendChatGoneNotification(JID to, JID from) { Message m = new Message(); m.setType(Message.Type.chat); m.setTo(to); m.setFrom(from); if (JiveGlobals.getBooleanProperty("plugin.gateway.globsl.messageeventing", true)) { Element xEvent = m.addChildElement("x", "jabber:x:event"); xEvent.addElement("id"); } m.addChildElement("gone", NameSpace.CHATSTATES); sendPacket(m); } /** * Sends a active chat session notification through the component manager. * Note that this type of state is also included in chat messages with a * body. Take care to avoid sending duplicate 'active' state messages. This * method is intended to be used only for those (rare) occurrances, where * the legacy domain wishes to express that a contact is activly taking part * in a conversation, without sending an actual chat message. * * This will check whether the person supports typing notifications before * sending. TODO: actually check for typing notification support * * @param to * Who the notification is for. * @param from * Who the notification is from. */ public void sendChatActiveNotification(JID to, JID from) { final Message m = new Message(); m.setType(Message.Type.chat); m.setTo(to); m.setFrom(from); if (JiveGlobals.getBooleanProperty("plugin.gateway.globsl.messageeventing", true)) { final Element xEvent = m.addChildElement("x", "jabber:x:event"); xEvent.addElement("id"); } m.addChildElement("active", NameSpace.CHATSTATES); sendPacket(m); } /** * Intercepts roster additions related to the gateway and flags them as non-persistent. * * @see org.jivesoftware.openfire.roster.RosterEventListener#addingContact(org.jivesoftware.openfire.roster.Roster, org.jivesoftware.openfire.roster.RosterItem, boolean) */ public boolean addingContact(Roster roster, RosterItem item, boolean persistent) { if (item.getJid().getDomain().equals(this.getJID().toString()) && item.getJid().getNode() != null && !JiveGlobals.getBooleanProperty("plugin.gateway.tweak.persistentroster", false)) { return false; } return persistent; } /** * Handles updates to a roster item that are not normally forwarded to the transport. * * @see org.jivesoftware.openfire.roster.RosterEventListener#contactUpdated(org.jivesoftware.openfire.roster.Roster, org.jivesoftware.openfire.roster.RosterItem) */ public void contactUpdated(Roster roster, RosterItem item) { //TODO: Disabling this for now // if (!item.getJid().getDomain().equals(this.getJID().getDomain())) { // // Not ours, not our problem. // return; // } // if (item.getJid().getNode() == null) { // // Gateway itself, don't care. // return; // } // if (item.getGroups() == null) { // // We got a null grouplist? That's not reasonable. // return; // } // try { // TransportSession<B> session = sessionManager.getSession(roster.getUsername()); // if (!session.isRosterLocked(item.getJid().toString())) { // Log.debug(getType().toString()+": contactUpdated "+roster.getUsername()+":"+item.getJid()); // TransportBuddy buddy = session.getBuddyManager().getBuddy(item.getJid()); // if (buddy != null) { // buddy.setNicknameAndGroups(item.getNickname(), item.getGroups()); // } // else { // Log.debug("Updating contact "+item.getJid()+" for "+roster.getUsername()+" failed, unable to locate."); // } // } // } // catch (NotFoundException e) { // // Well we just don't care then. // } } /** * Handles additions to a roster. We don't really care because we hear about these via subscribes. * * @see org.jivesoftware.openfire.roster.RosterEventListener#contactAdded(org.jivesoftware.openfire.roster.Roster, org.jivesoftware.openfire.roster.RosterItem) */ public void contactAdded(Roster roster, RosterItem item) { //TODO: Disabling this for now // if (!item.getJid().getDomain().equals(this.getJID().getDomain())) { // // Not ours, not our problem. // return; // } // if (item.getJid().getNode() == null) { // // Gateway itself, don't care. // return; // } // try { // TransportSession<B> session = sessionManager.getSession(roster.getUsername()); // if (!session.isRosterLocked(item.getJid().toString())) { // Log.debug(getType().toString()+": contactAdded "+roster.getUsername()+":"+item.getJid()); // session.addContact(item); // } // } // catch (NotFoundException e) { // // Well we just don't care then. // } } /** * Handles deletions from a roster. We don't really care because we hear about these via unsubscribes. * * @see org.jivesoftware.openfire.roster.RosterEventListener#contactDeleted(org.jivesoftware.openfire.roster.Roster, org.jivesoftware.openfire.roster.RosterItem) */ public void contactDeleted(Roster roster, RosterItem item) { //TODO: Disabling this for now // if (!item.getJid().getDomain().equals(this.getJID().getDomain())) { // // Not ours, not our problem. // return; // } // if (item.getJid().getNode() == null) { // // Gateway itself, don't care. // return; // } // try { // TransportSession<B> session = sessionManager.getSession(roster.getUsername()); // if (!session.isRosterLocked(item.getJid().toString())) { // Log.debug(getType().toString()+": contactDeleted "+roster.getUsername()+":"+item.getJid()); // session.getBuddyManager().removeBuddy(convertJIDToID(item.getJid())); // } // } // catch (NotFoundException e) { // // Well we just don't care then. // } } /** * Handles notifications of a roster being loaded. Not sure we care. * * @see org.jivesoftware.openfire.roster.RosterEventListener#rosterLoaded(org.jivesoftware.openfire.roster.Roster) */ public void rosterLoaded(Roster roster) { Log.debug(getType().toString()+": rosterLoaded "+roster.getUsername()); // Don't care } /** * Handles a user being deleted from the server. * * @see org.jivesoftware.openfire.event.UserEventListener#userDeleting(org.jivesoftware.openfire.user.User, java.util.Map) */ public void userDeleting(User user, Map<String,Object> params) { try { new RegistrationHandler(this).deleteRegistration(XMPPServer .getInstance().createJID(user.getUsername(), null)); } catch (UserNotFoundException e) { // Not our problem then. } } /** * Handles a user being added to the server. We do not care. * * @see org.jivesoftware.openfire.event.UserEventListener#userCreated(org.jivesoftware.openfire.user.User, java.util.Map) */ public void userCreated(User user, Map<String,Object> params) { // Don't care. } /** * Handles a user being modified on the server. We do not care. * * @see org.jivesoftware.openfire.event.UserEventListener#userModified(org.jivesoftware.openfire.user.User, java.util.Map) */ public void userModified(User user, Map<String,Object> params) { // Don't care } /** * Handles a session going offline. Apparently presence isn't entirely reliable... * * @see org.jivesoftware.openfire.event.SessionEventListener#sessionDestroyed(org.jivesoftware.openfire.session.Session) */ public void sessionDestroyed(Session destroyedSession) { JID from = destroyedSession.getAddress(); // A user's resource has gone offline. TransportSession<B> session; try { session = sessionManager.getSession(from); if (session.getLoginStatus().equals(TransportLoginStatus.LOGGING_OUT) || session.getLoginStatus().equals(TransportLoginStatus.LOGGED_OUT)) { // This is being taken care of by something else then. return; } String resource = from.getResource(); if (session.hasResource(resource)) { if (session.getResourceCount() > 1) { // Just one of the resources, lets adjust accordingly. if (session.isHighestPriority(resource)) { Log.debug("A high priority resource (of multiple) has gone offline (NoPres): " + from); // Ooh, the highest resource went offline, drop to next highest. session.removeResource(resource); // Lets ask the next highest resource what it's presence is. Presence p = new Presence(Presence.Type.probe); p.setTo(session.getJIDWithHighestPriority()); p.setFrom(this.getJID()); sendPacket(p); } else { Log.debug("A low priority resource (of multiple) has gone offline (NoPres): " + from); // Meh, lower priority, big whoop. session.removeResource(resource); } } else { Log.debug("A final resource has gone offline (NoPres): " + from); // No more resources, byebye. this.registrationLoggedOut(session); sessionManager.removeSession(from); } } } catch (NotFoundException e) { // Ignore } } /** * Handles a resource getting bound. We do not care. * * @see org.jivesoftware.openfire.event.SessionEventListener#resourceBound(org.jivesoftware.openfire.session.Session) */ public void resourceBound(Session session) { // Do nothing. } /** * Handles a session coming online. We do not care. * * @see org.jivesoftware.openfire.event.SessionEventListener#sessionCreated(org.jivesoftware.openfire.session.Session) */ public void sessionCreated(Session session) { // Don't care } /** * Handles an anonymous session going offline. We do not care. * * @see org.jivesoftware.openfire.event.SessionEventListener#anonymousSessionDestroyed(org.jivesoftware.openfire.session.Session) */ public void anonymousSessionDestroyed(Session session) { try { new RegistrationHandler(this).deleteRegistration(XMPPServer .getInstance().createJID(session.getAddress().getNode(), null)); } catch (UserNotFoundException e) { // Not our problem then. } } /** * Handles an anonymous session coming online. We do not care. * * @see org.jivesoftware.openfire.event.SessionEventListener#anonymousSessionCreated(org.jivesoftware.openfire.session.Session) */ public void anonymousSessionCreated(Session session) { // Don't care } /** * VCard was just created. * * @see org.jivesoftware.openfire.vcard.VCardListener#vCardCreated(String, Element) */ public void vCardCreated(String username, Element vcardElem) { if (vcardElem != null) { if (JiveGlobals.getBooleanProperty("plugin.gateway."+getType()+".avatars", true)) { Element photoElem = vcardElem.element("PHOTO"); if (photoElem != null) { Element typeElem = photoElem.element("TYPE"); Element binElem = photoElem.element("BINVAL"); if (typeElem != null && binElem != null) { try { MessageDigest md = MessageDigest.getInstance("SHA-1"); byte[] imageData = Base64.decode(binElem.getText()); md.update(imageData); String xmppHash = StringUtils.encodeHex(md.digest()); try { TransportSession<B> trSession = sessionManager.getSession(username); if (trSession.getAvatar() == null || !trSession.getAvatar().getXmppHash().equals(xmppHash)) { // Store a cache of the avatar trSession.setAvatar(new Avatar(trSession.getJID(), imageData)); trSession.updateLegacyAvatar(typeElem.getText(), imageData); } } catch (NotFoundException e) { // Not an active session, ignore. } } catch (NoSuchAlgorithmException e) { Log.error("Gateway: Unable to find support for SHA algorith?"); } } } } } } /** * VCard was just updated. * * @see org.jivesoftware.openfire.vcard.VCardListener#vCardUpdated(String, Element) */ public void vCardUpdated(String username, Element vcardElem) { if (vcardElem != null) { if (JiveGlobals.getBooleanProperty("plugin.gateway."+getType()+".avatars", true)) { Element photoElem = vcardElem.element("PHOTO"); if (photoElem != null) { Element typeElem = photoElem.element("TYPE"); Element binElem = photoElem.element("BINVAL"); if (typeElem != null && binElem != null) { try { MessageDigest md = MessageDigest.getInstance("SHA-1"); byte[] imageData = Base64.decode(binElem.getText()); md.update(imageData); String xmppHash = StringUtils.encodeHex(md.digest()); try { TransportSession<B> trSession = sessionManager.getSession(username); if (trSession.getAvatar() == null || !trSession.getAvatar().getXmppHash().equals(xmppHash)) { // Store a cache of the avatar trSession.setAvatar(new Avatar(trSession.getJID(), imageData)); trSession.updateLegacyAvatar(typeElem.getText(), imageData); } } catch (NotFoundException e) { // Not an active session, ignore. } } catch (NoSuchAlgorithmException e) { Log.error("Gateway: Unable to find support for SHA algorith?"); } } } } } } /** * VCard was just deleted. * * @see org.jivesoftware.openfire.vcard.VCardListener#vCardDeleted(String, Element) */ public void vCardDeleted(String username, Element vcardElem) { Log.debug("vCardDeleted: "+username); // TODO: Handle this for potentially an avatar undo } /** * Intercepts disco items packets to filter out users who aren't allowed to register. * * @see org.jivesoftware.openfire.interceptor.PacketInterceptor#interceptPacket(org.xmpp.packet.Packet, org.jivesoftware.openfire.session.Session, boolean, boolean) */ @SuppressWarnings("unchecked") public void interceptPacket(Packet packet, Session session, boolean incoming, boolean processed) { // If not IQ, return immediately. if (!(packet instanceof IQ)) { return; } // If it's a result IQ, process for possible filtering. if (((IQ)packet).getType().equals(IQ.Type.result)) { // If the packet is not outgoing back to the user or not processed yet, we don't care. if (processed || incoming) { return; } // If not query, return immediately. Element child = packet.getElement().element("query"); if (child == null) { return; } // If no namespace uri, return immediately. if (child.getNamespaceURI() == null) { return; } // If not disco#items, return immediately. if (!child.getNamespaceURI().equals(NameSpace.DISCO_ITEMS)) { return; } // If the node is null, we don't care, not directly related to a user. JID to = packet.getTo(); if (to.getNode() == null) { return; } JID from = packet.getFrom(); // If not from server itself, return immediately. if (!XMPPServer.getInstance().isLocal(from)) { return; } // If user registered, return immediately. if (RegistrationManager.getInstance().isRegistered(to, transportType)) { return; } // Check if allowed, if so return immediately. if (permissionManager.hasAccess(to)) { return; } // Filter out item associated with transport. Iterator iter = child.elementIterator(); while (iter.hasNext()) { Element elem = (Element)iter.next(); try { if (elem.attribute("jid").getText().equals(this.jid.toString())) { child.remove(elem); } } catch (Exception e) { // No worries. Wasn't what we were looking for. } } //TODO: should filter conference as well at some point. return; } // If it's a set IQ, process for possible roster activity. if (((IQ)packet).getType().equals(IQ.Type.set)) { // If the packet is not coming from the user, we don't care. if (!incoming) { return; } // If not query, return immediately. Element child = packet.getElement().element("query"); if (child == null) { return; } // If not jabber:iq:roster, return immediately. if (!child.getNamespaceURI().equals(NameSpace.IQ_ROSTER)) { return; } // Example items in roster modification. Iterator iter = child.elementIterator(); while (iter.hasNext()) { Element elem = (Element)iter.next(); if (!elem.getName().equals("item")) { continue; } String jidStr; String nickname = null; String sub = null; ArrayList<String> groups = new ArrayList<String>(); try { jidStr = elem.attributeValue("jid"); } catch (Exception e) { // No JID found, we don't want this then. continue; } JID jid = new JID(jidStr); if (!jid.getDomain().equals(this.getJID().toString())) { // Not for our domain, moving on. continue; } if (jid.getNode() == null) { // Gateway itself, don't care. return; } try { nickname = elem.attributeValue("name"); } catch (Exception e) { // No nickname, ok then. } try { sub = elem.attributeValue("subscription"); } catch (Exception e) { // No subscription, no worries. } Iterator groupIter = elem.elementIterator(); while (groupIter.hasNext()) { Element groupElem = (Element)groupIter.next(); if (!groupElem.getName().equals("group")) { continue; } groups.add(groupElem.getText()); } if (sub != null && sub.equals("remove")) { try { TransportSession trSession = sessionManager.getSession(session.getAddress().getNode()); if (!trSession.isRosterLocked(jid.toString())) { Log.debug(getType().toString()+": contact delete "+session.getAddress().getNode()+":"+jid); trSession.getBuddyManager().removeBuddy(convertJIDToID(jid)); } } catch (NotFoundException e) { // Well we just don't care then. } } else { try { TransportSession trSession = sessionManager.getSession(session.getAddress().getNode()); if (!trSession.isRosterLocked(jid.toString())) { try { TransportBuddy buddy = trSession.getBuddyManager().getBuddy(jid); Log.debug(getType().toString()+": contact update "+session.getAddress().getNode()+":"+jid+":"+nickname+":"+groups); buddy.setNicknameAndGroups(nickname, groups); } catch (NotFoundException e) { Log.debug(getType().toString()+": contact add "+session.getAddress().getNode()+":"+jid+":"+nickname+":"+groups); trSession.addContact(jid, nickname, groups); } } } catch (NotFoundException e) { // Well we just don't care then. } } } } } /** * Will handle logging in to the legacy service. * * @param registration Registration used for log in. * @param jid JID that is logged into the transport. * @param presenceType Type of presence. * @param verboseStatus Longer status description. * @param priority Priority of the session (from presence packet). * @return A session instance for the new login. */ public abstract TransportSession<B> registrationLoggedIn(Registration registration, JID jid, PresenceType presenceType, String verboseStatus, Integer priority); /** * Will handle logging out of the legacy service. * * @param session TransportSession to be logged out. */ public abstract void registrationLoggedOut(TransportSession<B> session); /** * Returns the terminology used for a username on the legacy service. * * @return String term for username. */ public abstract String getTerminologyUsername(); /** * Returns the terminology used for a password on the legacy service. * * @return String term for password. */ public abstract String getTerminologyPassword(); /** * Returns the terminology used for a nickname on the legacy service. * * You can return null to indicate that this is not supported by the legacy service. * * @return String term for nickname. */ public abstract String getTerminologyNickname(); /** * Returns instructions for registration in legacy complient terminology. * * You would write this out as if the entry textfields for the username and password * are after it/on the same page. So something along these lines would be good: * Please enter your legacy username and password. * * @return String phrase for registration. */ public abstract String getTerminologyRegistration(); /** * Is a password required for this service? * * @return True or false whether the password is required. */ public abstract Boolean isPasswordRequired(); /** * Is a nickname required for this service? * * @return True or false whether the nickname is required. */ public abstract Boolean isNicknameRequired(); /** * Is the specified username valid? * * @param username Username to validate. * @return True or false whether the passed username is valud for the service. */ public abstract Boolean isUsernameValid(String username); }