/* * Tigase Jabber/XMPP Server * Copyright (C) 2004-2012 "Artur Hefczyc" <artur.hefczyc@tigase.org> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. Look for COPYING file in the top folder. * If not, see http://www.gnu.org/licenses/. * * $Rev$ * Last modified by $Author$ * $Date$ */ package tigase.xmpp.impl; //~--- non-JDK imports -------------------------------------------------------- import tigase.db.NonAuthUserRepository; import tigase.db.TigaseDBException; import tigase.server.Packet; import tigase.server.Priority; import tigase.stats.StatisticsList; import tigase.sys.TigaseRuntime; import tigase.util.TigaseStringprepException; import tigase.xml.Element; import tigase.xmpp.Authorization; import tigase.xmpp.JID; import tigase.xmpp.NoConnectionIdException; import tigase.xmpp.NotAuthorizedException; import tigase.xmpp.PacketErrorTypeException; import tigase.xmpp.StanzaType; import tigase.xmpp.XMPPException; import tigase.xmpp.XMPPProcessor; import tigase.xmpp.XMPPProcessorIfc; import tigase.xmpp.XMPPResourceConnection; import tigase.xmpp.XMPPStopListenerIfc; import tigase.xmpp.impl.roster.RosterAbstract; import tigase.xmpp.impl.roster.RosterElement; import tigase.xmpp.impl.roster.RosterElementIfc; import tigase.xmpp.impl.roster.RosterFactory; import static tigase.xmpp.impl.roster.RosterAbstract.FROM_SUBSCRIBED; import static tigase.xmpp.impl.roster.RosterAbstract.TO_SUBSCRIBED; import static tigase.xmpp.impl.roster.RosterAbstract.PresenceType; import static tigase.xmpp.impl.roster.RosterAbstract.SUB_BOTH; import static tigase.xmpp.impl.roster.RosterAbstract.SUB_FROM; import static tigase.xmpp.impl.roster.RosterAbstract.SUB_TO; import static tigase.xmpp.impl.roster.RosterAbstract.SUB_NONE; import static tigase.xmpp.impl.roster.RosterAbstract.SubscriptionType; //~--- JDK imports ------------------------------------------------------------ import java.util.Arrays; import java.util.EnumSet; import java.util.LinkedHashSet; import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import tigase.xmpp.BareJID; //~--- classes ---------------------------------------------------------------- /** * Describe class Presence here. * * * Created: Wed Feb 22 07:30:03 2006 * * @author <a href="mailto:artur.hefczyc@tigase.org">Artur Hefczyc</a> * @version $Rev$ */ public class Presence extends XMPPProcessor implements XMPPProcessorIfc, XMPPStopListenerIfc { /** * <code>DIRECT_PRESENCE</code> is a key in temporary session data for the * collection of JIDs where direct presence was sent. To all these addresses * unavailable presence must be sent when user disconnects. */ public static final String DIRECT_PRESENCE = "direct-presences"; /** Field description */ public static final String PRESENCE_ELEMENT_NAME = "presence"; /** Field description */ public static final String SKIP_OFFLINE_PROP_KEY = "skip-offline"; public static final String OFFLINE_ROSTER_LAST_SEEN_PROP_KEY = "offline-roster-last-seen"; public static final String PRESENCE_GLOBAL_FORWARD = "presence-global-forward"; protected static final String XMLNS = CLIENT_XMLNS; public static final String USERS_STATUS_CHANGES = "Users status changes"; /** * Private logger for class instance. */ private static final Logger log = Logger.getLogger(Presence.class.getName()); private static final String[] XMLNSS = { XMLNS }; private static final String[] ELEMENTS = { PRESENCE_ELEMENT_NAME }; private static final String ID = PRESENCE_ELEMENT_NAME; private static final long MAX_DIRECT_PRESENCES_NO = 1000; private static long requiredNo = 0; private static long requiredYes = 0; private static TigaseRuntime runtime = TigaseRuntime.getTigaseRuntime(); /** Field description */ public static final String OFFLINE_RES_SENT = "offline-res-sent"; /** Field description */ public static final String OFFLINE_BUD_SENT = "offline-bud-sent"; private static boolean skipOffline = false; private static int HIGH_PRIORITY_PRESENCES_NO = 10; // ~--- fields --------------------------------------------------------------- protected RosterAbstract roster_util = getRosterUtil(); private long usersStatusChanges = 0; private String[] offlineRosterLastSeen = null; private JID presenceGLobalForward = null; // ~--- methods -------------------------------------------------------------- /** * Method description * * * @param jid * @param session */ @SuppressWarnings({ "unchecked" }) public static void addDirectPresenceJID(JID jid, XMPPResourceConnection session) { Set<JID> direct_presences = (Set<JID>) session.getSessionData(DIRECT_PRESENCE); if (direct_presences == null) { direct_presences = new LinkedHashSet<JID>(10); session.putSessionData(DIRECT_PRESENCE, direct_presences); } // end of if (direct_presences == null) if (direct_presences.size() < MAX_DIRECT_PRESENCES_NO) { direct_presences.add(jid); } if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Added direct presence jid: {0}", jid); } } /** * <code>sendPresenceBroadcast</code> method broadcasts given presence to all * buddies from roster and to all users to which direct presence was sent. * * @param session * a <code>XMPPResourceConnection</code> value * @param results * @param settings * @param roster_util * @exception NotAuthorizedException * if an error occurs * @throws TigaseDBException */ public static void broadcastOffline(XMPPResourceConnection session, Queue<Packet> results, Map<String, Object> settings, RosterAbstract roster_util) throws NotAuthorizedException, TigaseDBException { // Preventing sending offline notifications more than once if (session.getSessionData(OFFLINE_BUD_SENT) != null) { return; } session.putSessionData(OFFLINE_BUD_SENT, OFFLINE_BUD_SENT); Element pres = session.getPresence(); if (pres != null) { // This code should not be necessary anymore. It is done in the stopped // method. // pres.setAttribute("type", StanzaType.unavailable.toString()); sendPresenceBroadcast(StanzaType.unavailable, session, FROM_SUBSCRIBED, results, pres, settings, roster_util); } else { broadcastDirectPresences(StanzaType.unavailable, session, results, pres); } // // String from = session.getJID(); // //String[] buddies = roster_util.getBuddies(session, FROM_SUBSCRIBED, // false); // JID[] buddies = roster_util.getBuddies(session, FROM_SUBSCRIBED); // // buddies = DynamicRoster.addBuddies(session, settings, buddies); // // // Only broadcast offline presence if there are any buddies expecting it // // and only if the user has sent initial presence before. // if ((buddies != null) && (pres != null)) { // // // Set<String> onlineJids = // TigaseRuntime.getTigaseRuntime().getOnlineJids(); // // Below code is not needed, this should be done while the presence // // is being processed and saved // // pres.setAttribute("from", from); // for (JID buddy : buddies) { // // // If buddy is a local buddy and he is offline, don't send him packet... // if (requiresPresenceSending(buddy, session)) { // sendPresence(StanzaType.unavailable, null, buddy, results, pres); // } // // // sendPresence(StanzaType.unavailable, from, buddy, results, pres); // } // end of for (String buddy: buddies) // } // end of if (buddies == null) // // broadcastDirectPresences(StanzaType.unavailable, session, results, pres); } /** * Method description * * * @param jid * @param session */ @SuppressWarnings({ "unchecked" }) public static void removeDirectPresenceJID(JID jid, XMPPResourceConnection session) { Set<JID> direct_presences = (Set<JID>) session.getSessionData(DIRECT_PRESENCE); if (direct_presences != null) { direct_presences.remove(jid); } // end of if (direct_presences == null) if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Added direct presence jid: {0}", jid); } } public static void sendPresence(StanzaType t, BareJID from, BareJID to, Queue<Packet> results, Element pres) { sendPresence(t, JID.jidInstance(from), JID.jidInstance(to), results, pres); } /** * Method description * * * @param t * @param from * @param to * @param results * @param pres */ public static Packet sendPresence(StanzaType t, JID from, JID to, Queue<Packet> results, Element pres) { Element presence = null; Packet result = null; if (pres == null) { presence = new Element(PRESENCE_ELEMENT_NAME); if (t != null) { presence.setAttribute("type", t.toString()); } // end of if (t != null) else { presence.setAttribute("type", StanzaType.unavailable.toString()); } // end of if (t != null) else presence.setAttribute("from", from.toString()); presence.setXMLNS(XMLNS); } else { presence = pres.clone(); } // end of if (pres == null) else presence.setAttribute("to", to.toString()); // Optimization, especially useful for cluster mode. // try getting all connection IDs connection manager addresses for the // destination packets if possible and send packets directly without // going through the session manager on other node. // Please note! may cause unneeded behavior if privacy lists or other // blocking mechanism is used // This is actually not such a good idea, it is always better to send the // packets through the SM. The only packets which could be optimized that // way // are Message packets. // JID[] connIds = runtime.getConnectionIdsForJid(to); // // if ((connIds != null) && (connIds.length > 0)) { // for (JID connId : connIds) { // try { // Packet packet = Packet.packetInstance(presence); // // packet.setPacketTo(connId); // // if (log.isLoggable(Level.FINEST)) { // log.finest("Sending presence info: " + packet); // } // // results.offer(packet); // } catch (TigaseStringprepException ex) { // log.warning("Packet stringprep addressing problem, skipping presence send: " // + presence); // } // } // } else { try { // Connection IDs are not available so let's send it a normal way result = Packet.packetInstance(presence); if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Sending presence info: {0}", result); } results.offer(result); } catch (TigaseStringprepException ex) { log.log(Level.FINE, "Packet stringprep addressing problem, skipping presence send: {0}", presence); } // } return result; } /** * <code>sendPresenceBroadcast</code> method broadcasts given presence to all * buddies from roster and to all users to which direct presence was sent. * * @param t * a <code>StanzaType</code> value * @param session * a <code>XMPPResourceConnection</code> value * @param subscrs * @param results * @param pres * an <code>Element</code> value * @param settings * @param roster_util * @exception NotAuthorizedException * if an error occurs * @throws TigaseDBException */ public static void sendPresenceBroadcast(StanzaType t, XMPPResourceConnection session, EnumSet<SubscriptionType> subscrs, Queue<Packet> results, Element pres, // final Map<String, Object> settings, boolean onlineOnly) Map<String, Object> settings, RosterAbstract roster_util) throws NotAuthorizedException, TigaseDBException { // Direct presence if any should be sent first broadcastDirectPresences(t, session, results, pres); // String from = session.getJID(); // String[] buddies = roster_util.getBuddies(session, subscrs, onlineOnly); RosterAbstract roster = roster_util; if (roster == null) { roster = RosterFactory.getRosterImplementation(true); } JID[] buddies = roster.getBuddies(session, subscrs); buddies = DynamicRoster.addBuddies(session, settings, buddies); if (buddies != null) { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Buddies found: " + Arrays.toString(buddies)); } Priority pack_priority = Priority.PRESENCE; int pres_cnt = 0; for (JID buddy : buddies) { if (requiresPresenceSending(roster, buddy, session)) { Packet pack = sendPresence(t, session.getJID(), buddy, results, pres); if (pres_cnt == HIGH_PRIORITY_PRESENCES_NO) { ++pres_cnt; pack_priority = Priority.LOWEST; } if (pack != null) { pack.setPriority(pack_priority); roster.setPresenceSent(session, buddy, true); } } else { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Not sending presence to buddy: " + buddy); } } } // end of for (String buddy: buddies) } else { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "No buddies found!!!!"); } } } /** * <code>updatePresenceChange</code> method is used to broadcast to all active * resources presence stanza received from other users, like incoming * availability presence, subscription presence and so on... Initial presences * are however sent only to those resources which already have sent initial * presence. * * @param presence * an <code>Element</code> presence received from other users, we * have to change 'to' attribute to full resource JID. * @param session * a <code>XMPPResourceConnection</code> value keeping connection * session object. * @param results * @exception NotAuthorizedException * if an error occurs */ public static void updatePresenceChange(Packet presence, XMPPResourceConnection session, Queue<Packet> results) throws NotAuthorizedException { boolean initial_p = ((presence.getAttribute("type") == null) || "available".equals(presence.getAttribute("type")) || "unavailable" .equals(presence.getAttribute("type"))); for (XMPPResourceConnection conn : session.getActiveSessions()) { // Update presence change only for online resources that is // resources which already sent initial presence. if (((conn.getPresence() == null) && initial_p)) { // Ignore.... if (log.isLoggable(Level.FINEST)) { log.finest("Skipping update presence change for a resource which hasn't sent " + "initial presence yet, or is remote connection: " + conn); } } else { try { if (log.isLoggable(Level.FINER)) { log.finer("Update presence change to: " + conn); } // Send to old resource presence about new resource Packet pres_update = presence.copyElementOnly(); pres_update.initVars(presence.getStanzaFrom(), conn.getJID() .copyWithoutResource()); pres_update.setPacketTo(conn.getConnectionId()); results.offer(pres_update); } catch (Exception e) { // It might be quite possible that one of the user connections // is in state not allowed for sending presence, in such a case // none of user connections would receive presence. // This catch is to make sure all other resources receive // notification. } } } // end of for (XMPPResourceConnection conn: sessions) } @Override public void getStatistics(StatisticsList list) { super.getStatistics(list); list.add(id(), USERS_STATUS_CHANGES, usersStatusChanges, Level.INFO); } /** * <code>updateUserResources</code> method is used to broadcast to all * <strong>other</strong> resources presence stanza from one user resource. So * if new resource connects this method updates presence information about new * resource to old resources and about old resources to new resource. * * @param presence * an <code>Element</code> presence received from other users, we * have to change 'to' attribute to full resource JID. * @param session * a <code>XMPPResourceConnection</code> value keeping connection * session object. * @param results * @exception NotAuthorizedException * if an error occurs */ public static void updateUserResources(Element presence, XMPPResourceConnection session, Queue<Packet> results, boolean initial) throws NotAuthorizedException { for (XMPPResourceConnection conn : session.getActiveSessions()) { try { if (log.isLoggable(Level.FINER)) { log.log(Level.FINER, "Update presence change to: {0}", conn.getJID()); } // We also do not send presence updates to any remote connections on // different cluster nodes. Each node takes care of delivering presence // locally if (conn.isResourceSet()) { // Send to old resource presence about new resource Element pres_update = presence.clone(); // Below settings are applied by packetInstance(...) method a few // lines later // pres_update.setAttribute("from", session.getJID().toString()); // pres_update.setAttribute("to", conn.getBareJID().toString()); Packet pack_update = Packet.packetInstance(pres_update, session.getJID(), conn.getJID() .copyWithoutResource()); pack_update.setPacketTo(conn.getConnectionId()); results.offer(pack_update); Element presence_el = conn.getPresence(); // Send to new resource last presence sent by the old resource if (presence_el != null && initial && conn != session) { pres_update = presence_el.clone(); // Below is not necessary, initVars(...) which is called from // packetInstance() sets from/to attributes for stanza // pres_update.setAttribute("from", conn.getJID().toString()); // pres_update.setAttribute("to", session.getUserId().toString()); pack_update = Packet.packetInstance(pres_update, conn.getJID(), session.getJID() .copyWithoutResource()); pack_update.setPacketTo(session.getConnectionId()); results.offer(pack_update); } } else { if (log.isLoggable(Level.FINER)) { log.finer("Skipping presence update to: " + conn.getJID()); } } // end of else } catch (Exception e) { // It might be quite possible that one of the user connections // is in state not allowed for sending presence, in such a case // none of user connections would receive presence. // This catch is to make sure all other resources receive notification. } } // end of for (XMPPResourceConnection conn: sessions) } @SuppressWarnings({ "unchecked" }) protected static void broadcastDirectPresences(StanzaType t, XMPPResourceConnection session, Queue<Packet> results, Element pres) throws NotAuthorizedException, TigaseDBException { Set<JID> direct_presences = (Set<JID>) session.getSessionData(DIRECT_PRESENCE); if ((direct_presences != null) && (t != null) && (t == StanzaType.unavailable)) { for (JID buddy : direct_presences) { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Updating direct presence for: {0}", buddy); } Packet pack = sendPresence(t, session.getJID(), buddy, results, pres); pack.setPriority(Priority.LOW); } // end of for (String buddy: buddies) } // end of if (direct_presence != null) } protected static void forwardPresence(Queue<Packet> results, Packet packet, JID from) { Element result = packet.getElement().clone(); // Not needed anymore. Packet filter does it for all stanzas. // According to spec we must set proper FROM attribute // Yes, but packet filter puts full JID and we need a subscription // presence without resource here. result.setAttribute("from", from.toString()); if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "\n\nFORWARD presence: {0}", result.toString()); } results.offer(Packet.packetInstance(result, from, packet.getStanzaTo())); } /** * <code>updateOfflineChange</code> method broadcast off-line presence to all * other user active resources. * * @param session * a <code>XMPPResourceConnection</code> value * @param results * @exception NotAuthorizedException * if an error occurs */ protected static void updateOfflineChange(XMPPResourceConnection session, Queue<Packet> results) throws NotAuthorizedException { // Preventing sending offline notifications more than once if (session.getSessionData(OFFLINE_RES_SENT) != null) { return; } session.putSessionData(OFFLINE_RES_SENT, OFFLINE_RES_SENT); for (XMPPResourceConnection conn : session.getActiveSessions()) { try { if (log.isLoggable(Level.FINER)) { log.log(Level.FINER, "Update presence change to: {0}", conn.getJID()); } if ((conn != session) && (conn.isResourceSet()) && !conn.getResource().equals(session.getResource())) { // Send to old resource presence about new resource Element pres_update = new Element(PRESENCE_ELEMENT_NAME); // Below is set also by packetInstance method. // pres_update.setAttribute("from", session.getJID().toString()); // pres_update.setAttribute("to", conn.getBareJID().toString()); pres_update.setAttribute("type", StanzaType.unavailable.toString()); pres_update.setXMLNS(XMLNS); Packet pack_update = Packet.packetInstance(pres_update, session.getJID(), conn.getJID() .copyWithoutResource()); pack_update.setPacketTo(conn.getConnectionId()); results.offer(pack_update); } else { if (log.isLoggable(Level.FINER)) { log.log(Level.FINER, "Skipping presence update to: {0}", conn.getJID()); } } // end of else } catch (Exception e) { // It might be quite possible that one of the user connections // is in state not allowed for sending presence, in such a case // none of user connections would receive presence. // This catch is to make sure all other resources receive notification. } } // end of for (XMPPResourceConnection conn: sessions) } private static boolean requiresPresenceSending(RosterAbstract roster, JID buddy, XMPPResourceConnection session) throws NotAuthorizedException, TigaseDBException { boolean result = true; if (skipOffline && !roster.isOnline(session, buddy)) { result = false; } return result; } /** * <code>sendPresenceBroadcast</code> method broadcasts given presence to all * budies from roster and to all users to which direct presence was sent. * * @param session * a <code>XMPPResourceConnection</code> value * @param results * @param settings * @exception NotAuthorizedException * if an error occurs * @throws TigaseDBException */ public void broadcastProbe(XMPPResourceConnection session, Queue<Packet> results, Map<String, Object> settings) throws NotAuthorizedException, TigaseDBException { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Broadcasting probes for: {0}", session); } // Probe is always broadcasted with initial presence Element presInit = session.getPresence(); Element presProbe = new Element(PRESENCE_ELEMENT_NAME); presProbe.setXMLNS(XMLNS); presProbe.setAttribute("type", StanzaType.probe.toString()); presProbe.setAttribute("from", session.getBareJID().toString()); // String[] buddies = roster_util.getBuddies(session, TO_SUBSCRIBED, false); // We send presence probe to TO_SUBSCRIBED and initial presence to // FROM_SUBSCRIBED. Most of buddies however are BOTH. So as optimalization // Let's process BOTH first and then TO_ONLY and FROM_ONLY separately JID[] buddies = roster_util.getBuddies(session, SUB_BOTH); buddies = DynamicRoster.addBuddies(session, settings, buddies); if (buddies != null) { for (JID buddy : buddies) { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Sending probe to: {0}", buddy); } sendPresence(null, null, buddy, results, presProbe); if (requiresPresenceSending(roster_util, buddy, session)) { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Sending intial to: {0}", buddy); } sendPresence(null, null, buddy, results, presInit); roster_util.setPresenceSent(session, buddy, true); } } // end of for (String buddy: buddies) } // end of if (buddies == null) JID[] buddies_to = roster_util.getBuddies(session, SUB_TO); if (buddies_to != null) { for (JID buddy : buddies_to) { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Sending probe to: {0}", buddy); } sendPresence(null, null, buddy, results, presProbe); } // end of for (String buddy: buddies) } // end of if (buddies == null) // TODO: It might be a marginal number of cases here but just make it clear // we send a presence here regardless JID[] buddies_from = roster_util.getBuddies(session, SUB_FROM); if (buddies_from != null) { for (JID buddy : buddies_from) { if (requiresPresenceSending(roster_util, buddy, session)) { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Sending initial to: {0}", buddy); } sendPresence(null, null, buddy, results, presInit); roster_util.setPresenceSent(session, buddy, true); } } // end of for (String buddy: buddies) } // end of if (buddies == null) } /** * Method description * * * @return */ @Override public int concurrentQueuesNo() { return Runtime.getRuntime().availableProcessors() * 2; } /** * Method description * * * @return */ @Override public String id() { return ID; } /** * Method description * * * @param settings * * @throws TigaseDBException */ @Override public void init(Map<String, Object> settings) throws TigaseDBException { // Init plugin configuration skipOffline = Boolean.parseBoolean((String) settings.get(SKIP_OFFLINE_PROP_KEY)); String tmp = (String) settings.get(OFFLINE_ROSTER_LAST_SEEN_PROP_KEY); if (tmp != null) { offlineRosterLastSeen = tmp.split(","); log.config("Loaded roster offline last seen config: " + tmp); } else { log.config("No configuration found for Loaded roster offline last seen."); } tmp = (String) settings.get(PRESENCE_GLOBAL_FORWARD); if (tmp != null) { try { presenceGLobalForward = JID.jidInstance(tmp); } catch (TigaseStringprepException ex) { presenceGLobalForward = null; log.warning("Presence global forward misconfiguration, cannot parse JID " + tmp); } } } /** * Method description * * * @param packet * @param session * @param repo * @param results * @param settings * * @throws XMPPException */ @SuppressWarnings({ "unchecked", "fallthrough" }) @Override public void process(final Packet packet, final XMPPResourceConnection session, final NonAuthUserRepository repo, final Queue<Packet> results, final Map<String, Object> settings) throws XMPPException { if (session == null) { if (log.isLoggable(Level.FINE)) { log.log(Level.FINE, "Session is null, ignoring packet: {0}", packet); } return; } // end of if (session == null) if (!session.isAuthorized()) { if (log.isLoggable(Level.FINE)) { log.log(Level.FINE, "Session is not authorized, ignoring packet: {0}", packet); } return; } // Synchronization to avoid conflict with login/logout events // processed in the SessionManager asynchronously synchronized (session) { try { PresenceType pres_type = roster_util.getPresenceType(session, packet); if (pres_type == null) { log.log(Level.INFO, "Invalid presence found: {0}", packet); return; } // end of if (type == null) StanzaType type = packet.getType(); if (type == null) { type = StanzaType.available; } // end of if (type == null) // Not needed anymore. Packet filter does it for all stanzas. // // For all messages coming from the owner of this account set // // proper 'from' attribute // if (packet.getFrom().equals(session.getConnectionId())) { // packet.getElement().setAttribute("from", session.getJID()); // } // end of if (packet.getFrom().equals(session.getConnectionId())) if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "{0} presence found: {1}", new Object[] { pres_type, packet }); } // All 'in' subscription presences must have a valid from address switch (pres_type) { case in_unsubscribe: case in_subscribe: case in_unsubscribed: case in_subscribed: if (packet.getStanzaFrom() == null) { if (log.isLoggable(Level.FINE)) { log.fine("'in' subscription presence without valid 'from' address, " + "dropping packet: " + packet); } return; } if (session.isUserId(packet.getStanzaFrom().getBareJID())) { if (log.isLoggable(Level.FINE)) { log.log(Level.FINE, "''in'' subscription to myself, not allowed, returning " + "error for packet: " + "{0}", packet); } results.offer(Authorization.NOT_ALLOWED.getResponseMessage(packet, "You can not subscribe to yourself.", false)); return; } break; case out_subscribe: case out_unsubscribe: case out_subscribed: case out_unsubscribed: // Check wheher the destination address is correct to prevent // broken/corrupted roster entries: if ((packet.getStanzaTo() == null) || packet.getStanzaTo().toString().isEmpty()) { results.offer(Authorization.JID_MALFORMED.getResponseMessage(packet, "The destination address is incorrect.", false)); return; } // According to RFC 3921 draft bis-3, both source and destination // addresses must be BareJIDs // No need for that, initVars(...) takes care of that // packet.getElement().setAttribute("from", // session.getJID().copyWithoutResource().toString()); // packet.getElement().setAttribute("to", // packet.getStanzaTo().copyWithoutResource().toString()); packet.initVars(session.getJID().copyWithoutResource(), packet.getStanzaTo() .copyWithoutResource()); break; default: break; } switch (pres_type) { case out_initial: processOutInitial(packet, session, results, settings, pres_type); break; case out_subscribe: case out_unsubscribe: processOutSubscribe(packet, session, results, settings, pres_type); break; case out_subscribed: case out_unsubscribed: processOutSubscribed(packet, session, results, settings, pres_type); break; case in_initial: String resource = packet.getStanzaTo().getResource(); if (session.getPresence() == null && ((resource == null) || resource.isEmpty())) { // If the user has not yet sent initial presence then ignore all // incoming // presences except direct presences. return; } if (type == StanzaType.unavailable) { roster_util.setOnline(session, packet.getStanzaFrom(), false); } else { buddyOnline(session, packet.getStanzaFrom(), results); } processInInitial(packet, session, results, settings, pres_type); break; case in_subscribe: processInSubscribe(packet, session, results, settings, pres_type); break; case in_unsubscribe: processInUnsubscribe(packet, session, results, settings, pres_type); break; case in_subscribed: processInSubscribed(packet, session, results, settings, pres_type); break; case in_unsubscribed: processInUnsubscribed(packet, session, results, settings, pres_type); break; case in_probe: if (session.getPresence() == null) { // If the user has not yet sent initial presence then ignore the // probe. return; } processInProbe(packet, session, results, settings, pres_type); // this is actually already handled in processInProbe // buddyOnline(session, packet.getStanzaFrom(), results); break; case error: processError(packet, session, results, settings, pres_type); break; default: results.offer(Authorization.BAD_REQUEST.getResponseMessage(packet, "Request type is incorrect", false)); break; } // end of switch (type) } catch (NotAuthorizedException e) { log.log(Level.INFO, "Can not access user Roster, user session is not authorized yet: {0}", packet); log.log(Level.FINEST, "presence problem...", e); // results.offer(Authorization.NOT_AUTHORIZED.getResponseMessage(packet, // "You must authorize session first.", true)); } catch (TigaseDBException e) { log.log(Level.WARNING, "Error accessing database for presence data: {0}", e); } // end of try-catch } } /** * Method description * * * @param session * @param results * * @throws NotAuthorizedException * @throws TigaseDBException */ public void resendPendingInRequests(XMPPResourceConnection session, Queue<Packet> results) throws NotAuthorizedException, TigaseDBException { JID[] buddies = roster_util.getBuddies(session, RosterAbstract.PENDING_IN); if (buddies != null) { for (JID buddy : buddies) { Element presence = new Element(PRESENCE_ELEMENT_NAME); presence.setAttribute("type", StanzaType.subscribe.toString()); // presence.setAttribute("from", buddy.toString()); presence.setXMLNS(XMLNS); Packet pres = Packet.packetInstance(presence, buddy, null); updatePresenceChange(pres, session, results); } } } /** * <code>stopped</code> method is called when user disconnects or logs-out. * * @param session * a <code>XMPPResourceConnection</code> value * @param results * @param settings */ @Override public void stopped(XMPPResourceConnection session, Queue<Packet> results, Map<String, Object> settings) { // Synchronization to avoid conflict with login/logout events // processed in the SessionManager asynchronously synchronized (session) { // According to the spec and logic actually offline status should // not be broadcasted if initial presence was not sent by the client. try { if (session.getPresence() != null) { // If this was called without sending unavailable presence // we have to generate it on our own. Element pres = session.getPresence(); if (!StanzaType.unavailable.toString().equals(pres.getAttribute("type"))) { pres.setAttribute("type", StanzaType.unavailable.toString()); session.setPresence(pres); } broadcastOffline(session, results, settings, roster_util); updateOfflineChange(session, results); } else { broadcastDirectPresences(StanzaType.unavailable, session, results, null); } roster_util.logout(session); } catch (NotAuthorizedException e) { // Do nothing, it may happen quite often when the user disconnects // before // it authenticates } catch (TigaseDBException e) { log.log(Level.WARNING, "Error accessing database for offline message: ", e); } // end of try-catch } } /** * Method description * * * @return */ @Override public String[] supElements() { return ELEMENTS; } /** * Method description * * * @return */ @Override public String[] supNamespaces() { return XMLNSS; } protected RosterAbstract getRosterUtil() { return RosterFactory.getRosterImplementation(true); } protected void processError(Packet packet, XMPPResourceConnection session, Queue<Packet> results, Map<String, Object> settings, PresenceType presenceType) throws NotAuthorizedException, TigaseDBException, NoConnectionIdException { // Strategy change. // Now we allow all error presences sent to the user, but we just ignore // presence errors sent from the user if (session.isUserId(packet.getStanzaTo().getBareJID())) { Packet result = packet.copyElementOnly(); result.setPacketTo(session.getConnectionId()); result.setPacketFrom(packet.getTo()); results.offer(result); } else { // Ignore.... } } protected void processInInitial(Packet packet, XMPPResourceConnection session, Queue<Packet> results, Map<String, Object> settings, PresenceType presenceType) throws NoConnectionIdException, NotAuthorizedException, TigaseDBException { if (packet.getStanzaFrom() == null) { // That really happened already. It looks like a bug in tigase // let's try to catch it here.... log.log(Level.WARNING, "Initial presence without from attribute set: {0}", packet); return; } // If this is a direct presence to a resource which is already gone // Ignore it. String resource = packet.getStanzaTo().getResource(); if ((resource != null) && !resource.isEmpty()) { XMPPResourceConnection direct = session.getParentSession().getResourceForResource(resource); if (direct != null) { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Received direct presence from: {0} to: {1}", new Object[] { packet.getStanzaFrom(), packet.getStanzaTo() }); } // Send a direct presence to correct resource, otherwise ignore Packet result = packet.copyElementOnly(); result.setPacketTo(direct.getConnectionId()); result.setPacketFrom(packet.getTo()); results.offer(result); } else { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Ignoring direct presence from: {0} to: {1}, resource gone.", new Object[] { packet.getStanzaFrom(), packet.getStanzaTo() }); } } return; } if (session.getPresence() == null) { // Just ignore, this user does not want to receive presence updates return; } JID presBuddy = packet.getStanzaFrom().copyWithoutResource(); // If other users are in 'to' or 'both' contacts, broadcast // their presences to all active resources if (roster_util.isSubscribedTo(session, presBuddy) || (DynamicRoster.getBuddyItem(session, settings, presBuddy) != null) // || // // This might be just unsubscribed buddy // (roster_util.isBuddyOnline(session, presBuddy)) ) { boolean online = StanzaType.unavailable != packet.getType(); if (online) { RosterElementIfc rel = roster_util.getRosterElement(session, presBuddy); if (rel != null && rel instanceof RosterElement) { ((RosterElement) rel).setLastSeen(System.currentTimeMillis()); } } if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Received initial presence, setting buddy: {0} online status to: {1}", new Object[] { packet.getStanzaFrom(), online }); } roster_util.setOnline(session, packet.getStanzaFrom(), online); updatePresenceChange(packet, session, results); if (skipOffline && !roster_util.presenceSent(session, packet.getStanzaFrom())) { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Presence not yet sent to this buddy, sending: {0}", new Object[] { packet.getStanzaFrom() }); } sendPresence(null, null, packet.getStanzaFrom().copyWithoutResource(), results, session.getPresence()); roster_util.setPresenceSent(session, packet.getStanzaFrom(), true); } } else { } } protected void processInProbe(Packet packet, XMPPResourceConnection session, Queue<Packet> results, Map<String, Object> settings, PresenceType presenceType) throws NotAuthorizedException, TigaseDBException, PacketErrorTypeException { SubscriptionType buddy_subscr = null; if (DynamicRoster.getBuddyItem(session, settings, packet.getStanzaFrom()) != null) { buddy_subscr = SubscriptionType.both; } else { buddy_subscr = roster_util.getBuddySubscription(session, packet.getStanzaFrom()); } if (buddy_subscr == null) { buddy_subscr = SubscriptionType.none; } // end of if (buddy_subscr == null) if (roster_util.isSubscribedFrom(buddy_subscr)) { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Received probe, setting buddy: {0} as online.", packet.getStanzaFrom()); } roster_util.setOnline(session, packet.getStanzaFrom(), true); for (XMPPResourceConnection conn : session.getActiveSessions()) { try { Element pres = conn.getPresence(); if (pres != null) { sendPresence(null, null, packet.getStanzaFrom().copyWithoutResource(), results, pres); roster_util.setPresenceSent(session, packet.getStanzaFrom(), true); if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Received probe, sending presence response to: {0}", packet.getStanzaFrom()); } } } catch (Exception e) { // It might be quite possible that one of the user connections // is in state not allowed for sending presence, in such a case // none of user connections would receive presence. // This catch is to make sure all other resources receive // notification. } } } // acording to spec 4.3.2. Server Processing of Inbound Presence Probe // http://xmpp.org/rfcs/rfc6121.html#presence-probe-inbound // If the user's bare JID is in the contact's roster with a subscription // state other // than "From", "From + Pending Out", or "Both", then the contact's server // SHOULD return a presence stanza of type "unsubscribed" else { if (log.isLoggable(Level.FINEST)) { log.log( Level.FINEST, "Received probe, users bare JID: {0} is not in the roster. Responding with unsubscribed", packet.getStanzaFrom().getBareJID()); } sendPresence(StanzaType.unsubscribed, session.getBareJID(), packet.getStanzaFrom() .getBareJID(), results, null); } // end of if (roster_util.isSubscribedFrom(session, packet.getElemFrom())) } protected void processInSubscribe(Packet packet, XMPPResourceConnection session, Queue<Packet> results, Map<String, Object> settings, PresenceType pres_type) throws NotAuthorizedException, TigaseDBException, NoConnectionIdException { // If the buddy is already subscribed then auto-reply with subscribed // presence stanza. if (roster_util.isSubscribedFrom(session, packet.getStanzaFrom())) { sendPresence(StanzaType.subscribed, session.getJID().copyWithoutResource(), packet.getStanzaFrom(), results, null); } else { SubscriptionType curr_sub = roster_util.getBuddySubscription(session, packet.getStanzaFrom()); if (curr_sub == null) { curr_sub = SubscriptionType.none; roster_util.addBuddy(session, packet.getStanzaFrom(), null, null, null); } // end of if (curr_sub == null) roster_util.updateBuddySubscription(session, pres_type, packet.getStanzaFrom()); updatePresenceChange(packet, session, results); } // end of else // We can't know that actually, this might come from offline storage // roster_util.setBuddyOnline(session, packet.getElemFrom(), true); } protected void processInSubscribed(Packet packet, XMPPResourceConnection session, Queue<Packet> results, Map<String, Object> settings, PresenceType pres_type) throws NotAuthorizedException, TigaseDBException, NoConnectionIdException { SubscriptionType curr_sub = roster_util.getBuddySubscription(session, packet.getStanzaFrom()); if (curr_sub == null) { curr_sub = SubscriptionType.none; roster_util.addBuddy(session, packet.getStanzaFrom(), null, null, null); } // end of if (curr_sub == null) boolean subscr_changed = roster_util.updateBuddySubscription(session, pres_type, packet.getStanzaFrom()); if (subscr_changed) { Packet forward_p = packet.copyElementOnly(); forward_p.setPacketTo(session.getConnectionId()); results.offer(forward_p); // updatePresenceChange(packet.getElement(), session, results); roster_util.updateBuddyChange(session, results, roster_util.getBuddyItem(session, packet.getStanzaFrom())); } // We can't know that actually, this might come from offline storage // roster_util.setBuddyOnline(session, packet.getElemFrom(), true); } protected void processInUnsubscribe(Packet packet, XMPPResourceConnection session, Queue<Packet> results, Map<String, Object> settings, PresenceType pres_type) throws NotAuthorizedException, TigaseDBException, NoConnectionIdException { boolean subscr_changed = roster_util.updateBuddySubscription(session, pres_type, packet.getStanzaFrom()); if (subscr_changed) { // First forward the request to the client to make sure it stays in sync // with the server. // this should be done only in the case of actual change of the state Packet forward_p = packet.copyElementOnly(); forward_p.setPacketTo(session.getConnectionId()); results.offer(forward_p); // No longer needed according to RFC-3921-bis5 // sendPresence(StanzaType.unsubscribed, session.getJID(), // packet.getElemFrom(), // results, null); // updatePresenceChange(packet.getElement(), session, results); Element item = roster_util.getBuddyItem(session, packet.getStanzaFrom()); if (item != null) { roster_util.updateBuddyChange(session, results, item); } else { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Received unsubscribe request from a user who is not in the roster: {0}", packet.getStanzaFrom()); } } } } protected void processInUnsubscribed(Packet packet, XMPPResourceConnection session, Queue<Packet> results, Map<String, Object> settings, PresenceType pres_type) throws NotAuthorizedException, TigaseDBException, NoConnectionIdException { SubscriptionType curr_sub = roster_util.getBuddySubscription(session, packet.getStanzaFrom()); if (curr_sub != null) { // First forward the request to the client to make sure it stays in sync // with // the server. Packet forward_p = packet.copyElementOnly(); forward_p.setPacketTo(session.getConnectionId()); results.offer(forward_p); boolean subscr_changed = roster_util.updateBuddySubscription(session, pres_type, packet.getStanzaFrom()); if (subscr_changed) { // No longer needed according to RFC-3921-bis5 // updatePresenceChange(packet.getElement(), session, results); Element item = roster_util.getBuddyItem(session, packet.getStanzaFrom()); // The roster item could have been removed in the meantime.... if (item != null) { roster_util.updateBuddyChange(session, results, roster_util.getBuddyItem(session, packet.getStanzaFrom())); } else { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Received unsubscribe request from a user who is not in the roster: {0}", packet.getStanzaFrom()); } } } } } /** * Method sends server generated presence unavailable for all buddies from the * roster with a custom status message. * * @param session * @throws TigaseDBException * @throws NotAuthorizedException * @throws NoConnectionIdException */ protected void sendRosterOfflinePresence(XMPPResourceConnection session, Queue<Packet> results) throws NotAuthorizedException, TigaseDBException, NoConnectionIdException { if (offlineRosterLastSeen == null) { log.finest("No clients specified in config, skipping..."); return; } Element pres = session.getPresence(); if (pres == null) { log.finest("Presence not set yet, skipping..."); return; } String node = pres.getAttribute("/presence/c", "node"); if (node == null) { log.finest("Presence node not set, skipping..."); return; } boolean validClient = false; int i = 0; while (!(validClient |= node.contains(offlineRosterLastSeen[i++]))); if (!validClient) { log.finest("Client does not match, skipping..."); return; } JID[] buddies = roster_util.getBuddies(session, TO_SUBSCRIBED); if (buddies != null) { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Buddies found: " + Arrays.toString(buddies)); } Priority pack_priority = Priority.PRESENCE; int pres_cnt = 0; for (JID buddy : buddies) { String status = roster_util.getCustomStatus(session, buddy); if (status != null) { Packet pack = sendPresence(StanzaType.unavailable, buddy, session.getJID(), results, null); if (pres_cnt == HIGH_PRIORITY_PRESENCES_NO) { ++pres_cnt; pack_priority = Priority.LOWEST; } pack.setPriority(pack_priority); pack.setPacketTo(session.getConnectionId()); pack.getElement().addChild(new Element("status", status)); } } // end of for (String buddy: buddies) } } protected void processOutInitial(Packet packet, XMPPResourceConnection session, Queue<Packet> results, Map<String, Object> settings, PresenceType type) throws NotAuthorizedException, TigaseDBException { // if (session != null) { // throw new NullPointerException("THIS IS A TEST!!"); // } // Is it a direct presence to some entity on the network? if (packet.getStanzaTo() != null) { // Yes this is it, send direct presence // if (session.isAnonymous()) { // outInitialAnonymous(packet, session, results); // } results.offer(packet.copyElementOnly()); // If this is unavailable presence, remove jid from Set // otherwise add it to the Set if (packet.getType() == StanzaType.unavailable) { removeDirectPresenceJID(packet.getStanzaTo(), session); } else { addDirectPresenceJID(packet.getStanzaTo(), session); } } else { ++usersStatusChanges; boolean first = false; if (session.getPresence() == null) { first = true; } // Set a correct from attribute // No need for that, initVars(...) takes care of that // packet.getElement().setAttribute("from", jid.toString()); packet.initVars(session.getJID(), packet.getStanzaTo()); // Store user presence for later time... // To send response to presence probes for example. session.setPresence(packet.getElement()); // Special actions on the first availability presence if ((packet.getType() == null) || (packet.getType() == StanzaType.available)) { session.removeSessionData(OFFLINE_BUD_SENT); session.removeSessionData(OFFLINE_RES_SENT); if (first) { try { sendRosterOfflinePresence(session, results); } catch (Exception ex) { log.log(Level.INFO, "Experimental code throws exception: ", ex); } // Send presence probes to 'to' or 'both' contacts broadcastProbe(session, results, settings); // Resend pending in subscription requests resendPendingInRequests(session, results); // Broadcast initial presence to 'from' or 'both' contacts // sendPresenceBroadcast(type, session, FROM_SUBSCRIBED, // results, packet.getElement(), settings, false); // These below broadcast is handled by presenceProbe method // to improve buddy checks.... // sendPresenceBroadcast(type, session, FROM_SUBSCRIBED, // results, packet.getElement(), settings); } else { // Broadcast initial presence to 'from' or 'both' contacts // sendPresenceBroadcast(type, session, FROM_SUBSCRIBED, // results, packet.getElement(), settings, false); sendPresenceBroadcast(StanzaType.available, session, FROM_SUBSCRIBED, results, packet.getElement(), settings, roster_util); } // Broadcast initial presence to other available user resources // Element presence = packet.getElement().clone(); // Already done above, don't need to set it again here // presence.setAttribute("from", session.getJID()); updateUserResources(packet.getElement(), session, results, first); } else { stopped(session, results, settings); } // Presence forwarding JID forwardTo = session.getDomain().getPresenceForward(); if (forwardTo == null) { forwardTo = presenceGLobalForward; } if (forwardTo != null) { sendPresence(null, session.getJID(), forwardTo, results, packet.getElement()); } } } protected void processOutSubscribe(Packet packet, XMPPResourceConnection session, Queue<Packet> results, Map<String, Object> settings, PresenceType pres_type) throws NotAuthorizedException, TigaseDBException, NoConnectionIdException { // According to RFC-3921 I must forward all these kind presence // requests, it allows to resynchronize // subscriptions in case of synchronization loss boolean subscr_changed = false; forwardPresence(results, packet, session.getJID().copyWithoutResource()); SubscriptionType current_subscription = roster_util.getBuddySubscription(session, packet.getStanzaTo()); if (pres_type == PresenceType.out_subscribe) { if (current_subscription == null) { roster_util.addBuddy(session, packet.getStanzaTo(), null, null, null); } // end of if (current_subscription == null) subscr_changed = roster_util.updateBuddySubscription(session, pres_type, packet.getStanzaTo()); if (subscr_changed) { roster_util.updateBuddyChange(session, results, roster_util.getBuddyItem(session, packet.getStanzaTo())); } // end of if (subscr_changed) } else { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "out_subscribe: current_subscription = " + current_subscription); } if (current_subscription != null) { subscr_changed = roster_util.updateBuddySubscription(session, pres_type, packet.getStanzaTo()); current_subscription = roster_util.getBuddySubscription(session, packet.getStanzaTo()); if (subscr_changed) { roster_util.updateBuddyChange(session, results, roster_util.getBuddyItem(session, packet.getStanzaTo())); } // end of if (subscr_changed) if (SUB_NONE.contains(current_subscription)) { roster_util.removeBuddy(session, packet.getStanzaTo()); } // end of if (current_subscription == null) } } } protected void processOutSubscribed(Packet packet, XMPPResourceConnection session, Queue<Packet> results, Map<String, Object> settings, PresenceType pres_type) throws NotAuthorizedException, TigaseDBException, NoConnectionIdException { // According to RFC-3921 I must forward all these kind presence // requests, it allows to re-synchronize // subscriptions in case of synchronization loss forwardPresence(results, packet, session.getJID().copyWithoutResource()); Element initial_presence = session.getPresence(); JID buddy = packet.getStanzaTo().copyWithoutResource(); boolean subscr_changed = roster_util.updateBuddySubscription(session, pres_type, buddy); if (subscr_changed) { roster_util.updateBuddyChange(session, results, roster_util.getBuddyItem(session, buddy)); if (initial_presence != null) { if (pres_type == PresenceType.out_subscribed) { sendPresence(StanzaType.available, null, buddy, results, initial_presence); roster_util.setPresenceSent(session, buddy, true); } else { sendPresence(StanzaType.unavailable, session.getJID(), buddy, results, null); } } // end of if (subscr_changed) } } protected void buddyOnline(XMPPResourceConnection session, JID buddy, Queue<Packet> results) throws NotAuthorizedException, TigaseDBException { roster_util.setOnline(session, buddy, true); if (skipOffline && !roster_util.presenceSent(session, buddy) && roster_util.isSubscribedFrom(session, buddy)) { Element pres = session.getPresence(); if (pres != null) { sendPresence(null, null, buddy, results, pres); roster_util.setPresenceSent(session, buddy, true); } } } } // Presence