/* * 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.muc; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; import net.sf.kraken.BaseTransport; import net.sf.kraken.roster.TransportBuddy; import net.sf.kraken.session.TransportSession; import net.sf.kraken.type.NameSpace; import org.apache.log4j.Logger; import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.QName; import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.LocaleUtils; import org.jivesoftware.util.NotFoundException; import org.xmpp.component.Component; import org.xmpp.component.ComponentManager; import org.xmpp.packet.IQ; import org.xmpp.packet.JID; import org.xmpp.packet.Message; import org.xmpp.packet.Packet; import org.xmpp.packet.PacketError; import org.xmpp.packet.Presence; import org.xmpp.packet.PacketError.Condition; /** * Base class for all MUC transports. * * This class should be implemented to provide support for MUC in a transport. It * is attached to a BaseTransport implementation and the two work together to handle * session management and such. Generally a good chunk of the work will still be done * by the BaseTransport based pieces. * * @author Daniel Henninger */ public abstract class BaseMUCTransport<B extends TransportBuddy> implements Component { static Logger Log = Logger.getLogger(BaseMUCTransport.class); /** * Create a new BaseMUCTransport instance. * * @param transport Transport to associate with this MUC transport. */ public BaseMUCTransport(BaseTransport<B> transport) { this.transport = transport; requestWatcher = new RequestWatcher(); timer.schedule(requestWatcher, requestCheckInterval, requestCheckInterval); } /* The transport we are associated with. */ public BaseTransport<B> transport; /** * Retrieves the transport we are associated with. * * @return Transport we are associated with. */ public BaseTransport<B> getTransport() { return transport; } /** * Retrieves the name of the MUC transport. * @see org.xmpp.component.Component#getName() */ public String getName() { return getTransport().getName(); } /** * Retrieves the description of the MUC transport. * @see org.xmpp.component.Component#getDescription() */ public String getDescription() { return getTransport().getDescription(); } /** * Returns the jid of the MUC transport. * * @return the jid of the MUC transport. */ public JID getJID() { return this.jid; } /* List of IQ requests that are waiting on a response. */ ConcurrentHashMap<IQ,Date> pendingIQRequests = new ConcurrentHashMap<IQ,Date>(); /** * Stores a pending request. * * @param packet IQ packet that we are storing. */ public void storePendingRequest(IQ packet) { pendingIQRequests.put(packet, new Date()); } /** * Retrieves a pending request or null if no such request is found. * * @param from JID that the request was originally sent from. * @param to JID that the request was originally sent to. * @param namespace IQ namespace to identify request. * @return Matching pending request or null. */ public IQ getPendingRequest(JID from, JID to, String namespace) { for (IQ request : pendingIQRequests.keySet()) { if (request.getTo().equals(to) && request.getFrom().toBareJID().equals(from.toBareJID())) { Element child = request.getChildElement(); if (child != null) { String xmlns = child.getNamespaceURI(); if (xmlns.equals(namespace)) { pendingIQRequests.remove(request); return request; } } } } return null; } /** * Retires a pending request. * * @param from JID that the request was originally sent from. * @param to JID that the request was originally sent to. * @param namespace IQ namespace to identify request. */ public void cancelPendingRequest(JID from, JID to, String namespace) { for (IQ request : pendingIQRequests.keySet()) { if (request.getTo().equals(to) && request.getFrom().toBareJID().equals(from.toBareJID())) { Element child = request.getChildElement(); if (child != null) { String xmlns = child.getNamespaceURI(); if (xmlns.equals(namespace)) { IQ result = IQ.createResultIQ(request); result.setError(PacketError.Condition.item_not_found); getTransport().sendPacket(result); pendingIQRequests.remove(request); return; } } } } } /** * Expires a pending request. * * @param request The request that will be expired */ public void expirePendingRequest(IQ request) { IQ result = IQ.createResultIQ(request); result.setError(PacketError.Condition.remote_server_timeout); getTransport().sendPacket(result); pendingIQRequests.remove(request); } /** * Expires all pending requests that have timed out. */ public void checkPendingExpirations() { Date now = new Date(); now.setTime(now.getTime() - requestTimeout); for (IQ request : pendingIQRequests.keySet()) { Date when = pendingIQRequests.get(request); if (now.after(when)) { expirePendingRequest(request); } } } /** * Timer to check for IQ request expirations. */ private Timer timer = new Timer(); /** * Interval at which requests are checked. */ private int requestCheckInterval = 10000; // 10 seconds /** * How long before requests time out. */ private int requestTimeout = 30000; // 30 seconds /** * The actual request checker task. */ private RequestWatcher requestWatcher; /** * Check for expired IQ requests. */ private class RequestWatcher extends TimerTask { /** * Expire any requests that have timed out. */ @Override public void run() { checkPendingExpirations(); } } /* Cached list of rooms from IRC */ final public ConcurrentHashMap<String,MUCTransportRoom> roomCache = new ConcurrentHashMap<String,MUCTransportRoom>(); /* Last room list update */ public Date roomCacheLastUpdate = null; /* How long before the room cache is considered expired. */ private int roomCacheTimeout = 600000; // 10 minutes /** * Updates the room cache update timestamp. */ public void updateRoomCacheTimestamp() { roomCacheLastUpdate = new Date(); } /** * Resets (clears) the room cache. */ public void clearRoomCache() { roomCache.clear(); } /** * Retrieve cached information about a room. * * @param room Name of room to retrieve information about. * @return MUCTransportRoom instance for room. */ public MUCTransportRoom getCachedRoom(String room) { return roomCache.get(room.toLowerCase()); } /** * Stores an entry in the room cache. * * @param room MUCTransportRoom instance that we'll be storing. */ public void cacheRoom(MUCTransportRoom room) { roomCache.put(room.getName().toLowerCase(), room); } /** * Retrieves a list of all cached rooms. * * @return Collection of all cached rooms. */ public Collection<MUCTransportRoom> getCachedRooms() { return roomCache.values(); } /** * Returns whether we need to trigger a list update. * * @return True or false if we have passed the timeout period. */ public Boolean isRoomCacheOutOfDate() { Date timeout = new Date(); timeout.setTime(timeout.getTime() - roomCacheTimeout); return (roomCacheLastUpdate == null || roomCacheLastUpdate.before(timeout)); } /** * Handles all incoming XMPP stanzas, passing them to individual * packet type handlers. * * @param packet The packet to be processed. */ public void processPacket(Packet packet) { 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(); try { TransportSession<B> session = getTransport().getSessionManager().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(getTransport().getType().toString().toUpperCase()))); reply.add(m); } else if (to.getNode() == null) { // Message to gateway itself. Throw away for now. } else { MUCTransportSession mucSession = session.getMUCSessionManager().getSession(to.getNode()); if (packet.getBody() != null) { if (to.getResource() == null) { // Message to room mucSession.sendMessage(packet.getBody()); } else { // Private message mucSession.sendPrivateMessage(to.getResource(), packet.getBody()); } } else { if (packet.getSubject() != null) { // Set topic of room mucSession.updateTopic(packet.getSubject()); } } } } 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(getTransport().getType().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 (packet.getType() == Presence.Type.error) { // We don't want to do anything with this. Ignore it. return reply; } try { TransportSession<B> session = getTransport().getSessionManager().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(getTransport().getType().toString().toUpperCase()))); reply.add(m); } else if (to.getNode() == null) { // Ignore undirected presence. } else if (to.getResource() != null) { // Presence to a specific resource. if (packet.getType() == Presence.Type.unavailable) { // Handle logout. try { MUCTransportSession mucSession = session.getMUCSessionManager().getSession(to.getNode()); mucSession.leaveRoom(); session.getMUCSessionManager().removeSession(to.getNode()); } catch (NotFoundException e) { // Not found? Well then no problem. } } else { // Handle login. try { MUCTransportSession mucSession = session.getMUCSessionManager().getSession(to.getNode()); // Active session. mucSession.updateStatus(this.getTransport().getPresenceType(packet)); } catch (NotFoundException e) { // No current session, lets create one. MUCTransportSession mucSession = createRoom(session, to.getNode(), to.getResource()); session.getMUCSessionManager().storeSession(to.getNode(), mucSession); mucSession.enterRoom(); } } } else { // Presence to the room itself. Return error as per protocol. Presence p = new Presence(); p.setError(Condition.jid_malformed); p.setType(Presence.Type.error); p.setTo(from); p.setFrom(to); reply.add(p); } } 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(getTransport().getType().toString().toUpperCase()))); reply.add(m); } 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(PacketError.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.MUC_ADMIN)) { reply.addAll(handleMUCAdmin(packet)); } else if (xmlns.equals(NameSpace.MUC_USER)) { reply.addAll(handleMUCUser(packet)); } else { Log.debug("Unable to handle iq request: " + xmlns); IQ error = IQ.createResultIQ(packet); error.setError(PacketError.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) { List<Packet> reply = new ArrayList<Packet>(); JID from = packet.getFrom(); JID to = packet.getTo(); if (packet.getTo().getNode() == null) { // Requested info from transport itself. IQ result = IQ.createResultIQ(packet); if (from.getNode() == null || getTransport().permissionManager.hasAccess(from)) { Element response = DocumentHelper.createElement(QName.get("query", NameSpace.DISCO_INFO)); response.addElement("identity") .addAttribute("category", "conference") .addAttribute("type", "text") .addAttribute("name", this.getDescription()); response.addElement("feature") .addAttribute("var", NameSpace.DISCO_INFO); response.addElement("feature") .addAttribute("var", NameSpace.DISCO_ITEMS); response.addElement("feature") .addAttribute("var", NameSpace.MUC); result.setChildElement(response); } else { result.setError(PacketError.Condition.forbidden); } reply.add(result); } else { // Ah, a request for information about a room. IQ result = IQ.createResultIQ(packet); try { TransportSession<B> session = getTransport().getSessionManager().getSession(from); if (session.isLoggedIn()) { storePendingRequest(packet); session.getRoomInfo(getTransport().convertJIDToID(to)); } else { // Not logged in? Not logged in then. result.setError(PacketError.Condition.forbidden); reply.add(result); } } catch (NotFoundException e) { // Not found? No active session then. result.setError(PacketError.Condition.forbidden); 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>(); JID from = packet.getFrom(); JID to = packet.getTo(); if (packet.getTo().getNode() == null) { // A request for a list of rooms IQ result = IQ.createResultIQ(packet); if (JiveGlobals.getBooleanProperty("plugin.gateway."+getTransport().getType()+".roomlist", false)) { try { TransportSession<B> session = getTransport().getSessionManager().getSession(from); if (session.isLoggedIn()) { storePendingRequest(packet); session.getRooms(); } } catch (NotFoundException e) { // Not found? No active session then. result.setError(PacketError.Condition.forbidden); reply.add(result); } } else { // Time to lie and tell them we have no rooms sendRooms(from, new ArrayList<MUCTransportRoom>()); } } else { // Ah, a request for members of a room. IQ result = IQ.createResultIQ(packet); try { TransportSession<B> session = getTransport().getSessionManager().getSession(from); if (session.isLoggedIn()) { storePendingRequest(packet); session.getRoomMembers(getTransport().convertJIDToID(to)); } else { // Not logged in? Not logged in then. result.setError(PacketError.Condition.forbidden); reply.add(result); } } catch (NotFoundException e) { // Not found? No active session then. result.setError(PacketError.Condition.forbidden); reply.add(result); } } return reply; } /** * Handle MUC admin requests. * * @param packet An IQ packet in the MUC admin namespace. * @return A list of IQ packets to be returned to the user. */ private List<Packet> handleMUCAdmin(IQ packet) { List<Packet> reply = new ArrayList<Packet>(); JID from = packet.getFrom(); JID to = packet.getTo(); Element query = (packet).getChildElement(); Element item = query.element("item"); String nick = item.attribute("nick").getText(); String role = item.attribute("role").getText(); try { TransportSession<B> session = getTransport().getSessionManager().getSession(from); if (session.isLoggedIn()) { try { MUCTransportSession mucSession = session.getMUCSessionManager().getSession(to.getNode()); if (packet.getTo().getNode() == null) { // Targeted at a room. } else { // Targeted at a specific user. if (nick != null && role != null) { if (role.equals("none")) { // This is a kick String reason = ""; Element reasonElem = item.element("reason"); if (reasonElem != null) { reason = reasonElem.getText(); } mucSession.kickUser(nick, reason); } } } } catch (NotFoundException e) { // Not found? No active session then. } } } catch (NotFoundException e) { // Not found? No active session then. } return reply; } /** * Handle MUC user requests. * * @param packet An IQ packet in the MUC admin namespace. * @return A list of IQ packets to be returned to the user. */ private List<Packet> handleMUCUser(IQ packet) { List<Packet> reply = new ArrayList<Packet>(); // JID from = packet.getFrom(); // JID to = packet.getTo(); if (packet.getTo().getNode() == null) { // Targeted at a room. } else { // Targeted at a specific user. } return reply; } /** * Sends a list of rooms as a response to a service discovery request. * * @param to JID we will be sending the response to. * @param rooms List of MUCTransportRoom objects to send as a response. */ public void sendRooms(JID to, Collection<MUCTransportRoom> rooms) { IQ request = getPendingRequest(to, this.getJID(), NameSpace.DISCO_ITEMS); if (request != null) { IQ result = IQ.createResultIQ(request); Element response = DocumentHelper.createElement(QName.get("query", NameSpace.DISCO_ITEMS)); for (MUCTransportRoom room : rooms) { Element item = response.addElement("item"); item.addAttribute("jid", room.getJid().toBareJID()); item.addAttribute("name", room.getName()); } result.setChildElement(response); this.sendPacket(result); } } /** * Sends information about a room as a response to a service discovery request. * * @param to JID we will be sending the response to. * @param roomjid JID of the room info was requested about. * @param room A MUCTransportRoom object containing information to return as a response. */ public void sendRoomInfo(JID to, JID roomjid, MUCTransportRoom room) { IQ request = getPendingRequest(to, roomjid, NameSpace.DISCO_INFO); if (request != null) { IQ result = IQ.createResultIQ(request); Element response = DocumentHelper.createElement(QName.get("query", NameSpace.DISCO_INFO)); response.addElement("identity") .addAttribute("category", "conference") .addAttribute("type", "text") .addAttribute("name", room.getName()); response.addElement("feature") .addAttribute("var", NameSpace.MUC); response.addElement("feature") .addAttribute("var", NameSpace.DISCO_INFO); response.addElement("feature") .addAttribute("var", NameSpace.DISCO_ITEMS); if (room.getPassword_protected()) { response.addElement("feature") .addAttribute("var", "muc_passwordprotected"); } if (room.getHidden()) { response.addElement("feature") .addAttribute("var", "muc_hidden"); } if (room.getTemporary()) { response.addElement("feature") .addAttribute("var", "muc_temporary"); } if (room.getOpen()) { response.addElement("feature") .addAttribute("var", "muc_open"); } if (!room.getModerated()) { response.addElement("feature") .addAttribute("var", "muc_unmoderated"); } if (!room.getAnonymous()) { response.addElement("feature") .addAttribute("var", "muc_nonanonymous"); } Element form = DocumentHelper.createElement(QName.get("x", NameSpace.XDATA)); form.addAttribute("type", "result"); form.addElement("field") .addAttribute("var", "FORM_TYPE") .addAttribute("type", "hidden") .addElement("value") .addCDATA("http://jabber.org/protocol/muc#roominfo"); if (room.getContact() != null) { form.addElement("field") .addAttribute("var", "muc#roominfo_contactjid") .addAttribute("label", "Contact Addresses") .addElement("value") .addCDATA(room.getContact().toString()); } if (room.getName() != null) { form.addElement("field") .addAttribute("var", "muc#roominfo_description") .addAttribute("label", "Short Description of Room") .addElement("value") .addCDATA(room.getName()); } if (room.getLanguage() != null) { form.addElement("field") .addAttribute("var", "muc#roominfo_lang") .addAttribute("label", "Natural Language for Room Discussions") .addElement("value") .addCDATA(room.getLanguage()); } if (room.getLog_location() != null) { form.addElement("field") .addAttribute("var", "muc#roominfo_logs") .addAttribute("label", "URL for Archived Discussion Logs") .addElement("value") .addCDATA(room.getLog_location()); } if (room.getOccupant_count() != null) { form.addElement("field") .addAttribute("var", "muc#roominfo_occupants") .addAttribute("label", "Current Number of Occupants in Room") .addElement("value") .addCDATA(room.getOccupant_count().toString()); } if (room.getTopic() != null) { form.addElement("field") .addAttribute("var", "muc#roominfo_subject") .addAttribute("label", "Current Subject or Discussion Topic in Room") .addElement("value") .addCDATA(room.getTopic()); } response.add(form); result.setChildElement(response); this.sendPacket(result); } } /** * Sends a list of rooms as a response to a service discovery request. * * @param to JID we will be sending the response to. * @param roomjid JID of the room info was requested about. * @param members List of MUCTransportRoomMember objects to send as a response. */ public void sendRoomMembers(JID to, JID roomjid, List<MUCTransportRoomMember> members) { IQ request = getPendingRequest(to, roomjid, NameSpace.DISCO_ITEMS); if (request != null) { IQ result = IQ.createResultIQ(request); Element response = DocumentHelper.createElement(QName.get("query", NameSpace.DISCO_ITEMS)); for (MUCTransportRoomMember member : members) { Element item = response.addElement("item"); item.addAttribute("jid", member.getJid().toBareJID()); } result.setChildElement(response); this.sendPacket(result); } } /** * Sends a packet through the component manager as the component. * * @param packet Packet to be sent. */ public void sendPacket(Packet packet) { Log.debug(getName()+": 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(msg); 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); } /** * Initializes the MUC transport. * @see org.xmpp.component.Component#initialize(org.xmpp.packet.JID, org.xmpp.component.ComponentManager) */ public void initialize(JID jid, ComponentManager componentManager) { this.jid = jid; this.componentManager = componentManager; } /** * JID of the transport in question. */ public JID jid = null; /** * Component Manager associated with transport. */ public ComponentManager componentManager = null; /** * Starts the MUC transport. * @see org.xmpp.component.Component#start() */ public void start() { } /** * Stops the MUC transport. * @see org.xmpp.component.Component#shutdown() */ public void shutdown() { requestWatcher.cancel(); timer.cancel(); } /** * Converts a legacy chatroom (and optional username) to a JID. * * @param roomname Name of room to be converted. * @param username Username to be converted to a JID. * @return The legacy username as a JID. */ public JID convertIDToJID(String roomname, String username) { if (JiveGlobals.getBooleanProperty("plugin.gateway.tweak.percenthack", false)) { return new JID(roomname.replace('@', '%').replace(" ", ""), this.jid.getDomain(), username); } else { return new JID(JID.escapeNode(roomname.replace(" ", "")), this.jid.getDomain(), username); } } /** * Handles creation of a connection to a room. * * This is expected to create the connection to a remote chat room and return the session * that will maintain it. * * @param transportSession Transport session we are attaching to. * @param roomname Name of room to connect to. * @param nickname Nickname to use in room. * @return Session instance that will handle the room interactions. */ public abstract MUCTransportSession createRoom(TransportSession<B> transportSession, String roomname, String nickname); }