diff options
Diffstat (limited to 'src/org/jivesoftware/smackx/ping')
6 files changed, 556 insertions, 0 deletions
diff --git a/src/org/jivesoftware/smackx/ping/PingFailedListener.java b/src/org/jivesoftware/smackx/ping/PingFailedListener.java new file mode 100644 index 0000000..4cda33b --- /dev/null +++ b/src/org/jivesoftware/smackx/ping/PingFailedListener.java @@ -0,0 +1,21 @@ +/** + * Copyright 2012 Florian Schmaus + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.ping; + +public interface PingFailedListener { + void pingFailed(); +}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/ping/PingManager.java b/src/org/jivesoftware/smackx/ping/PingManager.java new file mode 100644 index 0000000..6b4b48c --- /dev/null +++ b/src/org/jivesoftware/smackx/ping/PingManager.java @@ -0,0 +1,343 @@ +/** + * Copyright 2012-2013 Florian Schmaus + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.ping; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.ConnectionCreationListener; +import org.jivesoftware.smack.ConnectionListener; +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.filter.PacketIDFilter; +import org.jivesoftware.smack.filter.PacketTypeFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.ping.packet.Ping; +import org.jivesoftware.smackx.ping.packet.Pong; + +/** + * Implements the XMPP Ping as defined by XEP-0199. This protocol offers an + * alternative to the traditional 'white space ping' approach of determining the + * availability of an entity. The XMPP Ping protocol allows ping messages to be + * send in a more XML-friendly approach, which can be used over more than one + * hop in the communication path. + * + * @author Florian Schmaus + * @see <a href="http://www.xmpp.org/extensions/xep-0199.html">XEP-0199:XMPP + * Ping</a> + */ +public class PingManager { + + public static final String NAMESPACE = "urn:xmpp:ping"; + public static final String ELEMENT = "ping"; + + + private static Map<Connection, PingManager> instances = + Collections.synchronizedMap(new WeakHashMap<Connection, PingManager>()); + + static { + Connection.addConnectionCreationListener(new ConnectionCreationListener() { + public void connectionCreated(Connection connection) { + new PingManager(connection); + } + }); + } + + private ScheduledExecutorService periodicPingExecutorService; + private Connection connection; + private int pingInterval = SmackConfiguration.getDefaultPingInterval(); + private Set<PingFailedListener> pingFailedListeners = Collections + .synchronizedSet(new HashSet<PingFailedListener>()); + private ScheduledFuture<?> periodicPingTask; + protected volatile long lastSuccessfulPingByTask = -1; + + + // Ping Flood protection + private long pingMinDelta = 100; + private long lastPingStamp = 0; // timestamp of the last received ping + + // Timestamp of the last pong received, either from the server or another entity + // Note, no need to synchronize this value, it will only increase over time + private long lastSuccessfulManualPing = -1; + + private PingManager(Connection connection) { + ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection); + sdm.addFeature(NAMESPACE); + this.connection = connection; + init(); + } + + private void init() { + periodicPingExecutorService = new ScheduledThreadPoolExecutor(1); + PacketFilter pingPacketFilter = new PacketTypeFilter(Ping.class); + connection.addPacketListener(new PacketListener() { + /** + * Sends a Pong for every Ping + */ + public void processPacket(Packet packet) { + if (pingMinDelta > 0) { + // Ping flood protection enabled + long currentMillies = System.currentTimeMillis(); + long delta = currentMillies - lastPingStamp; + lastPingStamp = currentMillies; + if (delta < pingMinDelta) { + return; + } + } + Pong pong = new Pong((Ping)packet); + connection.sendPacket(pong); + } + } + , pingPacketFilter); + connection.addConnectionListener(new ConnectionListener() { + + @Override + public void connectionClosed() { + maybeStopPingServerTask(); + } + + @Override + public void connectionClosedOnError(Exception arg0) { + maybeStopPingServerTask(); + } + + @Override + public void reconnectionSuccessful() { + maybeSchedulePingServerTask(); + } + + @Override + public void reconnectingIn(int seconds) { + } + + @Override + public void reconnectionFailed(Exception e) { + } + }); + instances.put(connection, this); + maybeSchedulePingServerTask(); + } + + public static PingManager getInstanceFor(Connection connection) { + PingManager pingManager = instances.get(connection); + + if (pingManager == null) { + pingManager = new PingManager(connection); + } + + return pingManager; + } + + public void setPingIntervall(int pingIntervall) { + this.pingInterval = pingIntervall; + } + + public int getPingIntervall() { + return pingInterval; + } + + public void registerPingFailedListener(PingFailedListener listener) { + pingFailedListeners.add(listener); + } + + public void unregisterPingFailedListener(PingFailedListener listener) { + pingFailedListeners.remove(listener); + } + + public void disablePingFloodProtection() { + setPingMinimumInterval(-1); + } + + public void setPingMinimumInterval(long ms) { + this.pingMinDelta = ms; + } + + public long getPingMinimumInterval() { + return this.pingMinDelta; + } + + /** + * Pings the given jid and returns the IQ response which is either of + * IQ.Type.ERROR or IQ.Type.RESULT. If we are not connected or if there was + * no reply, null is returned. + * + * You should use isPingSupported(jid) to determine if XMPP Ping is + * supported by the user. + * + * @param jid + * @param pingTimeout + * @return + */ + public IQ ping(String jid, long pingTimeout) { + // Make sure we actually connected to the server + if (!connection.isAuthenticated()) + return null; + + Ping ping = new Ping(connection.getUser(), jid); + + PacketCollector collector = + connection.createPacketCollector(new PacketIDFilter(ping.getPacketID())); + + connection.sendPacket(ping); + + IQ result = (IQ) collector.nextResult(pingTimeout); + + collector.cancel(); + return result; + } + + /** + * Pings the given jid and returns the IQ response with the default + * packet reply timeout + * + * @param jid + * @return + */ + public IQ ping(String jid) { + return ping(jid, SmackConfiguration.getPacketReplyTimeout()); + } + + /** + * Pings the given Entity. + * + * Note that XEP-199 shows that if we receive a error response + * service-unavailable there is no way to determine if the response was send + * by the entity (e.g. a user JID) or from a server in between. This is + * intended behavior to avoid presence leaks. + * + * Always use isPingSupported(jid) to determine if XMPP Ping is supported + * by the entity. + * + * @param jid + * @return True if a pong was received, otherwise false + */ + public boolean pingEntity(String jid, long pingTimeout) { + IQ result = ping(jid, pingTimeout); + + if (result == null || result.getType() == IQ.Type.ERROR) { + return false; + } + pongReceived(); + return true; + } + + public boolean pingEntity(String jid) { + return pingEntity(jid, SmackConfiguration.getPacketReplyTimeout()); + } + + /** + * Pings the user's server. Will notify the registered + * pingFailedListeners in case of error. + * + * If we receive as response, we can be sure that it came from the server. + * + * @return true if successful, otherwise false + */ + public boolean pingMyServer(long pingTimeout) { + IQ result = ping(connection.getServiceName(), pingTimeout); + + if (result == null) { + for (PingFailedListener l : pingFailedListeners) { + l.pingFailed(); + } + return false; + } + // Maybe not really a pong, but an answer is an answer + pongReceived(); + return true; + } + + /** + * Pings the user's server with the PacketReplyTimeout as defined + * in SmackConfiguration. + * + * @return true if successful, otherwise false + */ + public boolean pingMyServer() { + return pingMyServer(SmackConfiguration.getPacketReplyTimeout()); + } + + /** + * Returns true if XMPP Ping is supported by a given JID + * + * @param jid + * @return + */ + public boolean isPingSupported(String jid) { + try { + DiscoverInfo result = + ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(jid); + return result.containsFeature(NAMESPACE); + } + catch (XMPPException e) { + return false; + } + } + + /** + * Returns the time of the last successful Ping Pong with the + * users server. If there was no successful Ping (e.g. because this + * feature is disabled) -1 will be returned. + * + * @return + */ + public long getLastSuccessfulPing() { + return Math.max(lastSuccessfulPingByTask, lastSuccessfulManualPing); + } + + protected Set<PingFailedListener> getPingFailedListeners() { + return pingFailedListeners; + } + + /** + * Cancels any existing periodic ping task if there is one and schedules a new ping task if pingInterval is greater + * then zero. + * + */ + protected synchronized void maybeSchedulePingServerTask() { + maybeStopPingServerTask(); + if (pingInterval > 0) { + periodicPingTask = periodicPingExecutorService.schedule(new ServerPingTask(connection), pingInterval, + TimeUnit.SECONDS); + } + } + + private void maybeStopPingServerTask() { + if (periodicPingTask != null) { + periodicPingTask.cancel(true); + periodicPingTask = null; + } + } + + private void pongReceived() { + lastSuccessfulManualPing = System.currentTimeMillis(); + } +} diff --git a/src/org/jivesoftware/smackx/ping/ServerPingTask.java b/src/org/jivesoftware/smackx/ping/ServerPingTask.java new file mode 100644 index 0000000..0901b8f --- /dev/null +++ b/src/org/jivesoftware/smackx/ping/ServerPingTask.java @@ -0,0 +1,77 @@ +/** + * Copyright 2012-2013 Florian Schmaus + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.ping; + +import java.lang.ref.WeakReference; +import java.util.Set; + +import org.jivesoftware.smack.Connection; + +class ServerPingTask implements Runnable { + + // This has to be a weak reference because IIRC all threads are roots + // for objects and we have a new thread here that should hold a strong + // reference to connection so that it can be GCed. + private WeakReference<Connection> weakConnection; + + private int delta = 1000; // 1 seconds + private int tries = 3; // 3 tries + + protected ServerPingTask(Connection connection) { + this.weakConnection = new WeakReference<Connection>(connection); + } + + public void run() { + Connection connection = weakConnection.get(); + if (connection == null) { + // connection has been collected by GC + // which means we can stop the thread by breaking the loop + return; + } + if (connection.isAuthenticated()) { + PingManager pingManager = PingManager.getInstanceFor(connection); + boolean res = false; + + for (int i = 0; i < tries; i++) { + if (i != 0) { + try { + Thread.sleep(delta); + } catch (InterruptedException e) { + // We received an interrupt + // This only happens if we should stop pinging + return; + } + } + res = pingManager.pingMyServer(); + // stop when we receive a pong back + if (res) { + pingManager.lastSuccessfulPingByTask = System.currentTimeMillis(); + break; + } + } + if (!res) { + Set<PingFailedListener> pingFailedListeners = pingManager.getPingFailedListeners(); + for (PingFailedListener l : pingFailedListeners) { + l.pingFailed(); + } + } else { + // Ping was successful, wind-up the periodic task again + pingManager.maybeSchedulePingServerTask(); + } + } + } +} diff --git a/src/org/jivesoftware/smackx/ping/packet/Ping.java b/src/org/jivesoftware/smackx/ping/packet/Ping.java new file mode 100644 index 0000000..fc5bbdf --- /dev/null +++ b/src/org/jivesoftware/smackx/ping/packet/Ping.java @@ -0,0 +1,38 @@ +/** + * Copyright 2012 Florian Schmaus + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.ping.packet; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.ping.PingManager; + +public class Ping extends IQ { + + public Ping() { + } + + public Ping(String from, String to) { + setTo(to); + setFrom(from); + setType(IQ.Type.GET); + setPacketID(getPacketID()); + } + + public String getChildElementXML() { + return "<" + PingManager.ELEMENT + " xmlns=\'" + PingManager.NAMESPACE + "\' />"; + } + +} diff --git a/src/org/jivesoftware/smackx/ping/packet/Pong.java b/src/org/jivesoftware/smackx/ping/packet/Pong.java new file mode 100644 index 0000000..9300db0 --- /dev/null +++ b/src/org/jivesoftware/smackx/ping/packet/Pong.java @@ -0,0 +1,45 @@ +/** + * Copyright 2012 Florian Schmaus + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.ping.packet; + +import org.jivesoftware.smack.packet.IQ; + +public class Pong extends IQ { + + /** + * Composes a Pong packet from a received ping packet. This basically swaps + * the 'from' and 'to' attributes. And sets the IQ type to result. + * + * @param ping + */ + public Pong(Ping ping) { + setType(IQ.Type.RESULT); + setFrom(ping.getTo()); + setTo(ping.getFrom()); + setPacketID(ping.getPacketID()); + } + + /* + * Returns the child element of the Pong reply, which is non-existent. This + * is why we return 'null' here. See e.g. Example 11 from + * http://xmpp.org/extensions/xep-0199.html#e2e + */ + public String getChildElementXML() { + return null; + } + +} diff --git a/src/org/jivesoftware/smackx/ping/provider/PingProvider.java b/src/org/jivesoftware/smackx/ping/provider/PingProvider.java new file mode 100644 index 0000000..ebe7669 --- /dev/null +++ b/src/org/jivesoftware/smackx/ping/provider/PingProvider.java @@ -0,0 +1,32 @@ +/** + * Copyright 2012 Florian Schmaus + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.ping.provider; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smackx.ping.packet.Ping; +import org.xmlpull.v1.XmlPullParser; + +public class PingProvider implements IQProvider { + + public IQ parseIQ(XmlPullParser parser) throws Exception { + // No need to use the ping constructor with arguments. IQ will already + // have filled out all relevant fields ('from', 'to', 'id'). + return new Ping(); + } + +} |