Home | History | Annotate | Download | only in ping
      1 /**
      2  * Copyright 2012-2013 Florian Schmaus
      3  *
      4  * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *     http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package org.jivesoftware.smackx.ping;
     18 
     19 import java.util.Collections;
     20 import java.util.HashSet;
     21 import java.util.Map;
     22 import java.util.Set;
     23 import java.util.WeakHashMap;
     24 import java.util.concurrent.ScheduledExecutorService;
     25 import java.util.concurrent.ScheduledFuture;
     26 import java.util.concurrent.ScheduledThreadPoolExecutor;
     27 import java.util.concurrent.TimeUnit;
     28 
     29 import org.jivesoftware.smack.Connection;
     30 import org.jivesoftware.smack.ConnectionCreationListener;
     31 import org.jivesoftware.smack.ConnectionListener;
     32 import org.jivesoftware.smack.PacketCollector;
     33 import org.jivesoftware.smack.PacketListener;
     34 import org.jivesoftware.smack.SmackConfiguration;
     35 import org.jivesoftware.smack.XMPPException;
     36 import org.jivesoftware.smack.filter.PacketFilter;
     37 import org.jivesoftware.smack.filter.PacketIDFilter;
     38 import org.jivesoftware.smack.filter.PacketTypeFilter;
     39 import org.jivesoftware.smack.packet.IQ;
     40 import org.jivesoftware.smack.packet.Packet;
     41 import org.jivesoftware.smackx.ServiceDiscoveryManager;
     42 import org.jivesoftware.smackx.packet.DiscoverInfo;
     43 import org.jivesoftware.smackx.ping.packet.Ping;
     44 import org.jivesoftware.smackx.ping.packet.Pong;
     45 
     46 /**
     47  * Implements the XMPP Ping as defined by XEP-0199. This protocol offers an
     48  * alternative to the traditional 'white space ping' approach of determining the
     49  * availability of an entity. The XMPP Ping protocol allows ping messages to be
     50  * send in a more XML-friendly approach, which can be used over more than one
     51  * hop in the communication path.
     52  *
     53  * @author Florian Schmaus
     54  * @see <a href="http://www.xmpp.org/extensions/xep-0199.html">XEP-0199:XMPP
     55  *      Ping</a>
     56  */
     57 public class PingManager {
     58 
     59     public static final String NAMESPACE = "urn:xmpp:ping";
     60     public static final String ELEMENT = "ping";
     61 
     62 
     63     private static Map<Connection, PingManager> instances =
     64             Collections.synchronizedMap(new WeakHashMap<Connection, PingManager>());
     65 
     66     static {
     67         Connection.addConnectionCreationListener(new ConnectionCreationListener() {
     68             public void connectionCreated(Connection connection) {
     69                 new PingManager(connection);
     70             }
     71         });
     72     }
     73 
     74     private ScheduledExecutorService periodicPingExecutorService;
     75     private Connection connection;
     76     private int pingInterval = SmackConfiguration.getDefaultPingInterval();
     77     private Set<PingFailedListener> pingFailedListeners = Collections
     78             .synchronizedSet(new HashSet<PingFailedListener>());
     79     private ScheduledFuture<?> periodicPingTask;
     80     protected volatile long lastSuccessfulPingByTask = -1;
     81 
     82 
     83     // Ping Flood protection
     84     private long pingMinDelta = 100;
     85     private long lastPingStamp = 0; // timestamp of the last received ping
     86 
     87     // Timestamp of the last pong received, either from the server or another entity
     88     // Note, no need to synchronize this value, it will only increase over time
     89     private long lastSuccessfulManualPing = -1;
     90 
     91     private PingManager(Connection connection) {
     92         ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection);
     93         sdm.addFeature(NAMESPACE);
     94         this.connection = connection;
     95         init();
     96     }
     97 
     98     private void init() {
     99         periodicPingExecutorService = new ScheduledThreadPoolExecutor(1);
    100         PacketFilter pingPacketFilter = new PacketTypeFilter(Ping.class);
    101         connection.addPacketListener(new PacketListener() {
    102             /**
    103              * Sends a Pong for every Ping
    104              */
    105             public void processPacket(Packet packet) {
    106                 if (pingMinDelta > 0) {
    107                     // Ping flood protection enabled
    108                     long currentMillies = System.currentTimeMillis();
    109                     long delta = currentMillies - lastPingStamp;
    110                     lastPingStamp = currentMillies;
    111                     if (delta < pingMinDelta) {
    112                         return;
    113                     }
    114                 }
    115                 Pong pong = new Pong((Ping)packet);
    116                 connection.sendPacket(pong);
    117             }
    118         }
    119         , pingPacketFilter);
    120         connection.addConnectionListener(new ConnectionListener() {
    121 
    122             @Override
    123             public void connectionClosed() {
    124                 maybeStopPingServerTask();
    125             }
    126 
    127             @Override
    128             public void connectionClosedOnError(Exception arg0) {
    129                 maybeStopPingServerTask();
    130             }
    131 
    132             @Override
    133             public void reconnectionSuccessful() {
    134                 maybeSchedulePingServerTask();
    135             }
    136 
    137             @Override
    138             public void reconnectingIn(int seconds) {
    139             }
    140 
    141             @Override
    142             public void reconnectionFailed(Exception e) {
    143             }
    144         });
    145         instances.put(connection, this);
    146         maybeSchedulePingServerTask();
    147     }
    148 
    149     public static PingManager getInstanceFor(Connection connection) {
    150         PingManager pingManager = instances.get(connection);
    151 
    152         if (pingManager == null) {
    153             pingManager = new PingManager(connection);
    154         }
    155 
    156         return pingManager;
    157     }
    158 
    159     public void setPingIntervall(int pingIntervall) {
    160         this.pingInterval = pingIntervall;
    161     }
    162 
    163     public int getPingIntervall() {
    164         return pingInterval;
    165     }
    166 
    167     public void registerPingFailedListener(PingFailedListener listener) {
    168         pingFailedListeners.add(listener);
    169     }
    170 
    171     public void unregisterPingFailedListener(PingFailedListener listener) {
    172         pingFailedListeners.remove(listener);
    173     }
    174 
    175     public void disablePingFloodProtection() {
    176         setPingMinimumInterval(-1);
    177     }
    178 
    179     public void setPingMinimumInterval(long ms) {
    180         this.pingMinDelta = ms;
    181     }
    182 
    183     public long getPingMinimumInterval() {
    184         return this.pingMinDelta;
    185     }
    186 
    187     /**
    188      * Pings the given jid and returns the IQ response which is either of
    189      * IQ.Type.ERROR or IQ.Type.RESULT. If we are not connected or if there was
    190      * no reply, null is returned.
    191      *
    192      * You should use isPingSupported(jid) to determine if XMPP Ping is
    193      * supported by the user.
    194      *
    195      * @param jid
    196      * @param pingTimeout
    197      * @return
    198      */
    199     public IQ ping(String jid, long pingTimeout) {
    200         // Make sure we actually connected to the server
    201         if (!connection.isAuthenticated())
    202             return null;
    203 
    204         Ping ping = new Ping(connection.getUser(), jid);
    205 
    206         PacketCollector collector =
    207                 connection.createPacketCollector(new PacketIDFilter(ping.getPacketID()));
    208 
    209         connection.sendPacket(ping);
    210 
    211         IQ result = (IQ) collector.nextResult(pingTimeout);
    212 
    213         collector.cancel();
    214         return result;
    215     }
    216 
    217     /**
    218      * Pings the given jid and returns the IQ response with the default
    219      * packet reply timeout
    220      *
    221      * @param jid
    222      * @return
    223      */
    224     public IQ ping(String jid) {
    225         return ping(jid, SmackConfiguration.getPacketReplyTimeout());
    226     }
    227 
    228     /**
    229      * Pings the given Entity.
    230      *
    231      * Note that XEP-199 shows that if we receive a error response
    232      * service-unavailable there is no way to determine if the response was send
    233      * by the entity (e.g. a user JID) or from a server in between. This is
    234      * intended behavior to avoid presence leaks.
    235      *
    236      * Always use isPingSupported(jid) to determine if XMPP Ping is supported
    237      * by the entity.
    238      *
    239      * @param jid
    240      * @return True if a pong was received, otherwise false
    241      */
    242     public boolean pingEntity(String jid, long pingTimeout) {
    243         IQ result = ping(jid, pingTimeout);
    244 
    245         if (result == null || result.getType() == IQ.Type.ERROR) {
    246             return false;
    247         }
    248         pongReceived();
    249         return true;
    250     }
    251 
    252     public boolean pingEntity(String jid) {
    253         return pingEntity(jid, SmackConfiguration.getPacketReplyTimeout());
    254     }
    255 
    256     /**
    257      * Pings the user's server. Will notify the registered
    258      * pingFailedListeners in case of error.
    259      *
    260      * If we receive as response, we can be sure that it came from the server.
    261      *
    262      * @return true if successful, otherwise false
    263      */
    264     public boolean pingMyServer(long pingTimeout) {
    265         IQ result = ping(connection.getServiceName(), pingTimeout);
    266 
    267         if (result == null) {
    268             for (PingFailedListener l : pingFailedListeners) {
    269                 l.pingFailed();
    270             }
    271             return false;
    272         }
    273         // Maybe not really a pong, but an answer is an answer
    274         pongReceived();
    275         return true;
    276     }
    277 
    278     /**
    279      * Pings the user's server with the PacketReplyTimeout as defined
    280      * in SmackConfiguration.
    281      *
    282      * @return true if successful, otherwise false
    283      */
    284     public boolean pingMyServer() {
    285         return pingMyServer(SmackConfiguration.getPacketReplyTimeout());
    286     }
    287 
    288     /**
    289      * Returns true if XMPP Ping is supported by a given JID
    290      *
    291      * @param jid
    292      * @return
    293      */
    294     public boolean isPingSupported(String jid) {
    295         try {
    296             DiscoverInfo result =
    297                 ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(jid);
    298             return result.containsFeature(NAMESPACE);
    299         }
    300         catch (XMPPException e) {
    301             return false;
    302         }
    303     }
    304 
    305     /**
    306      * Returns the time of the last successful Ping Pong with the
    307      * users server. If there was no successful Ping (e.g. because this
    308      * feature is disabled) -1 will be returned.
    309      *
    310      * @return
    311      */
    312     public long getLastSuccessfulPing() {
    313         return Math.max(lastSuccessfulPingByTask, lastSuccessfulManualPing);
    314     }
    315 
    316     protected Set<PingFailedListener> getPingFailedListeners() {
    317         return pingFailedListeners;
    318     }
    319 
    320     /**
    321      * Cancels any existing periodic ping task if there is one and schedules a new ping task if pingInterval is greater
    322      * then zero.
    323      *
    324      */
    325     protected synchronized void maybeSchedulePingServerTask() {
    326         maybeStopPingServerTask();
    327         if (pingInterval > 0) {
    328             periodicPingTask = periodicPingExecutorService.schedule(new ServerPingTask(connection), pingInterval,
    329                     TimeUnit.SECONDS);
    330         }
    331     }
    332 
    333     private void maybeStopPingServerTask() {
    334         if (periodicPingTask != null) {
    335             periodicPingTask.cancel(true);
    336             periodicPingTask = null;
    337         }
    338     }
    339 
    340     private void pongReceived() {
    341         lastSuccessfulManualPing = System.currentTimeMillis();
    342     }
    343 }
    344