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