• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
Connection.addConnectionCreationListener(new ConnectionCreationListener() { public void connectionCreated(Connection connection) { new PingManager(connection); } })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 
PingManager(Connection connection)91     private PingManager(Connection connection) {
92         ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection);
93         sdm.addFeature(NAMESPACE);
94         this.connection = connection;
95         init();
96     }
97 
init()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 
getInstanceFor(Connection connection)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 
setPingIntervall(int pingIntervall)159     public void setPingIntervall(int pingIntervall) {
160         this.pingInterval = pingIntervall;
161     }
162 
getPingIntervall()163     public int getPingIntervall() {
164         return pingInterval;
165     }
166 
registerPingFailedListener(PingFailedListener listener)167     public void registerPingFailedListener(PingFailedListener listener) {
168         pingFailedListeners.add(listener);
169     }
170 
unregisterPingFailedListener(PingFailedListener listener)171     public void unregisterPingFailedListener(PingFailedListener listener) {
172         pingFailedListeners.remove(listener);
173     }
174 
disablePingFloodProtection()175     public void disablePingFloodProtection() {
176         setPingMinimumInterval(-1);
177     }
178 
setPingMinimumInterval(long ms)179     public void setPingMinimumInterval(long ms) {
180         this.pingMinDelta = ms;
181     }
182 
getPingMinimumInterval()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      */
ping(String jid, long pingTimeout)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      */
ping(String jid)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      */
pingEntity(String jid, long pingTimeout)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 
pingEntity(String jid)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      */
pingMyServer(long pingTimeout)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      */
pingMyServer()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      */
isPingSupported(String jid)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      */
getLastSuccessfulPing()312     public long getLastSuccessfulPing() {
313         return Math.max(lastSuccessfulPingByTask, lastSuccessfulManualPing);
314     }
315 
getPingFailedListeners()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      */
maybeSchedulePingServerTask()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 
maybeStopPingServerTask()333     private void maybeStopPingServerTask() {
334         if (periodicPingTask != null) {
335             periodicPingTask.cancel(true);
336             periodicPingTask = null;
337         }
338     }
339 
pongReceived()340     private void pongReceived() {
341         lastSuccessfulManualPing = System.currentTimeMillis();
342     }
343 }
344