From 8c14f0cb55570b3ba845b1f03cb51a85fc05970d Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Fri, 4 Jan 2013 11:43:35 +0000 Subject: [PATCH] Added XEP-199 aka. "XMPP Ping" support to smack. Fixes SMACK-388. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/trunk@13384 b35dd754-fafc-0310-a699-88a17e54d16e --- build/resources/META-INF/smack-config.xml | 3 + build/resources/META-INF/smack.providers | 7 + .../smack/SmackConfiguration.java | 25 +- .../org/jivesoftware/smackx/packet/Ping.java | 38 ++ .../org/jivesoftware/smackx/packet/Pong.java | 45 +++ .../smackx/ping/PingFailedListener.java | 21 ++ .../jivesoftware/smackx/ping/PingManager.java | 340 ++++++++++++++++++ .../smackx/ping/ServerPingTask.java | 120 +++++++ .../smackx/provider/PingProvider.java | 32 ++ .../smackx/ping/PingPongTest.java | 38 ++ 10 files changed, 665 insertions(+), 4 deletions(-) create mode 100644 source/org/jivesoftware/smackx/packet/Ping.java create mode 100644 source/org/jivesoftware/smackx/packet/Pong.java create mode 100644 source/org/jivesoftware/smackx/ping/PingFailedListener.java create mode 100644 source/org/jivesoftware/smackx/ping/PingManager.java create mode 100644 source/org/jivesoftware/smackx/ping/ServerPingTask.java create mode 100644 source/org/jivesoftware/smackx/provider/PingProvider.java create mode 100644 test-unit/org/jivesoftware/smackx/ping/PingPongTest.java diff --git a/build/resources/META-INF/smack-config.xml b/build/resources/META-INF/smack-config.xml index 85b468c00..59a16fc78 100644 --- a/build/resources/META-INF/smack-config.xml +++ b/build/resources/META-INF/smack-config.xml @@ -30,5 +30,8 @@ 10000 + + + 1800000 diff --git a/build/resources/META-INF/smack.providers b/build/resources/META-INF/smack.providers index 9f0f4f15a..d6f15c4cf 100644 --- a/build/resources/META-INF/smack.providers +++ b/build/resources/META-INF/smack.providers @@ -237,6 +237,13 @@ org.jivesoftware.smackx.provider.AdHocCommandDataProvider + + + ping + urn:xmpp:ping + org.jivesoftware.smackx.provider.PingProvider + + bad-action http://jabber.org/protocol/commands diff --git a/source/org/jivesoftware/smack/SmackConfiguration.java b/source/org/jivesoftware/smack/SmackConfiguration.java index 6c4fadfe8..cf68c75b2 100644 --- a/source/org/jivesoftware/smack/SmackConfiguration.java +++ b/source/org/jivesoftware/smack/SmackConfiguration.java @@ -20,12 +20,16 @@ package org.jivesoftware.smack; -import org.xmlpull.mxp1.MXParser; -import org.xmlpull.v1.XmlPullParser; - import java.io.InputStream; import java.net.URL; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.List; +import java.util.Vector; + +import org.xmlpull.mxp1.MXParser; +import org.xmlpull.v1.XmlPullParser; /** * Represents the configuration of Smack. The configuration is used for: @@ -54,6 +58,8 @@ public final class SmackConfiguration { private static int localSocks5ProxyPort = 7777; private static int packetCollectorSize = 5000; + private static int defaultPingInterval = 1800000; // 30 min (30*60*1000) + private SmackConfiguration() { } @@ -103,6 +109,9 @@ public final class SmackConfiguration { else if (parser.getName().equals("packetCollectorSize")) { packetCollectorSize = parseIntProperty(parser, packetCollectorSize); } + else if (parser.getName().equals("defaultPingInterval")) { + defaultPingInterval = parseIntProperty(parser, defaultPingInterval); + } } eventType = parser.next(); } @@ -299,6 +308,14 @@ public final class SmackConfiguration { SmackConfiguration.localSocks5ProxyPort = localSocks5ProxyPort; } + public static int getDefaultPingInterval() { + return defaultPingInterval; + } + + public static void setDefaultPingInterval(int defaultPingInterval) { + SmackConfiguration.defaultPingInterval = defaultPingInterval; + } + private static void parseClassToLoad(XmlPullParser parser) throws Exception { String className = parser.nextText(); // Attempt to load the class so that the class can get initialized diff --git a/source/org/jivesoftware/smackx/packet/Ping.java b/source/org/jivesoftware/smackx/packet/Ping.java new file mode 100644 index 000000000..d49d9c590 --- /dev/null +++ b/source/org/jivesoftware/smackx/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.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/source/org/jivesoftware/smackx/packet/Pong.java b/source/org/jivesoftware/smackx/packet/Pong.java new file mode 100644 index 000000000..34cd1ae00 --- /dev/null +++ b/source/org/jivesoftware/smackx/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.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/source/org/jivesoftware/smackx/ping/PingFailedListener.java b/source/org/jivesoftware/smackx/ping/PingFailedListener.java new file mode 100644 index 000000000..4cda33b0c --- /dev/null +++ b/source/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/source/org/jivesoftware/smackx/ping/PingManager.java b/source/org/jivesoftware/smackx/ping/PingManager.java new file mode 100644 index 000000000..f47365b73 --- /dev/null +++ b/source/org/jivesoftware/smackx/ping/PingManager.java @@ -0,0 +1,340 @@ +/** + * 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; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; + +import org.jivesoftware.smack.AbstractConnectionListener; +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.ConnectionCreationListener; +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.smack.provider.ProviderManager; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.packet.Ping; +import org.jivesoftware.smackx.packet.Pong; +import org.jivesoftware.smackx.provider.PingProvider; + +/** + * 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 XEP-0199:XMPP + * Ping + */ +public class PingManager { + + public static final String NAMESPACE = "urn:xmpp:ping"; + public static final String ELEMENT = "ping"; + + private static Map instances = + Collections.synchronizedMap(new WeakHashMap()); + + static { + ProviderManager.getInstance().addIQProvider(ELEMENT, NAMESPACE, new PingProvider()); + Connection.addConnectionCreationListener(new ConnectionCreationListener() { + public void connectionCreated(Connection connection) { + new PingManager(connection); + } + }); + } + + private Connection connection; + private Thread serverPingThread; + private ServerPingTask serverPingTask; + private int pingInterval = SmackConfiguration.getDefaultPingInterval(); + private Set pingFailedListeners = Collections + .synchronizedSet(new HashSet()); + + // Ping Flood protection + private long pingMinDelta = 100; + private long lastPingStamp = 0; // timestamp of the last received ping + + // Last server pong timestamp if a ping request manually + private long lastServerPingStamp = -1; + + private PingManager(Connection connection) { + ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection); + sdm.addFeature(NAMESPACE); + this.connection = connection; + PacketFilter pingPacketFilter = new PacketTypeFilter(Ping.class); + connection.addPacketListener(new PingPacketListener(), pingPacketFilter); + connection.addConnectionListener(new PingConnectionListener()); + instances.put(connection, this); + maybeStartPingServerTask(); + } + + public static PingManager getInstaceFor(Connection connection) { + PingManager pingManager = instances.get(connection); + + if (pingManager == null) { + pingManager = new PingManager(connection); + } + + return pingManager; + } + + public void setPingIntervall(int pingIntervall) { + this.pingInterval = pingIntervall; + if (serverPingTask != null) { + serverPingTask.setPingInterval(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; + } + 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; + } + lastServerPingStamp = System.currentTimeMillis(); + 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() { + long taskLastSuccessfulPing = -1; + if (serverPingTask != null) { + taskLastSuccessfulPing = serverPingTask.getLastSucessfulPing(); + } + return Math.max(taskLastSuccessfulPing, lastServerPingStamp); + } + + protected Set getPingFailedListeners() { + return pingFailedListeners; + } + + private class PingConnectionListener extends AbstractConnectionListener { + + @Override + public void connectionClosed() { + maybeStopPingServerTask(); + } + + @Override + public void connectionClosedOnError(Exception arg0) { + maybeStopPingServerTask(); + } + + @Override + public void reconnectionSuccessful() { + maybeStartPingServerTask(); + } + + } + + private void maybeStartPingServerTask() { + if (serverPingTask != null) { + serverPingTask.setDone(); + serverPingThread.interrupt(); + serverPingTask = null; + serverPingThread = null; + System.err.println("Smack PingManger: Found existing serverPingTask"); + } + + if (pingInterval > 0) { + serverPingTask = new ServerPingTask(connection, pingInterval); + serverPingThread = new Thread(serverPingTask); + serverPingThread.setDaemon(true); + serverPingThread.setName("Smack Ping Server Task (" + connection.getServiceName() + ")"); + serverPingThread.start(); + } + } + + private void maybeStopPingServerTask() { + if (serverPingThread != null) { + serverPingTask.setDone(); + serverPingThread.interrupt(); + } + } + + private class PingPacketListener implements PacketListener { + + public PingPacketListener() { + } + + /** + * 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); + } + } +} diff --git a/source/org/jivesoftware/smackx/ping/ServerPingTask.java b/source/org/jivesoftware/smackx/ping/ServerPingTask.java new file mode 100644 index 000000000..31c5a1e41 --- /dev/null +++ b/source/org/jivesoftware/smackx/ping/ServerPingTask.java @@ -0,0 +1,120 @@ +/** + * 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; + +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 weakConnection; + private int pingInterval; + private volatile long lastSuccessfulPing = -1; + + private int delta = 1000; // 1 seconds + private int tries = 3; // 3 tries + + protected ServerPingTask(Connection connection, int pingIntervall) { + this.weakConnection = new WeakReference(connection); + this.pingInterval = pingIntervall; + } + + protected void setDone() { + this.pingInterval = -1; + } + + protected void setPingInterval(int pingIntervall) { + this.pingInterval = pingIntervall; + } + + protected int getIntInterval() { + return pingInterval; + } + + protected long getLastSucessfulPing() { + return lastSuccessfulPing; + } + + public void run() { + sleep(60000); + + outerLoop: + while(pingInterval > 0) { + Connection connection = weakConnection.get(); + if (connection == null) { + // connection has been collected by GC + // which means we can stop the thread by breaking the loop + break; + } + if (connection.isAuthenticated()) { + PingManager pingManager = PingManager.getInstaceFor(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 + break outerLoop; + } + } + res = pingManager.pingMyServer(); + // stop when we receive a pong back + if (res) { + lastSuccessfulPing = System.currentTimeMillis(); + break; + } + } + if (!res) { + Set pingFailedListeners = pingManager.getPingFailedListeners(); + for (PingFailedListener l : pingFailedListeners) { + l.pingFailed(); + } + } + } + sleep(); + } + } + + /* + * If pingInterval > 0 sleeps a minimum of pingInterval + */ + private void sleep(int extraSleepTime) { + int totalSleep = pingInterval + extraSleepTime; + if (totalSleep > 0) { + try { + Thread.sleep(totalSleep); + } catch (InterruptedException e) { + /* Ignore */ + } + } + } + + /** + * Sleeps the amount of pingInterval + */ + private void sleep() { + sleep(0); + } +} diff --git a/source/org/jivesoftware/smackx/provider/PingProvider.java b/source/org/jivesoftware/smackx/provider/PingProvider.java new file mode 100644 index 000000000..0540fb91f --- /dev/null +++ b/source/org/jivesoftware/smackx/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.provider; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smackx.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(); + } + +} diff --git a/test-unit/org/jivesoftware/smackx/ping/PingPongTest.java b/test-unit/org/jivesoftware/smackx/ping/PingPongTest.java new file mode 100644 index 000000000..b11c22bff --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/ping/PingPongTest.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; + +import static org.junit.Assert.assertEquals; + +import org.jivesoftware.smackx.packet.Ping; +import org.jivesoftware.smackx.packet.Pong; +import org.junit.Test; + +public class PingPongTest { + + @Test + public void createPongfromPingTest() { + Ping ping = new Ping("from@sender.local/resourceFrom", "to@receiver.local/resourceTo"); + + // create a pong from a ping + Pong pong = new Pong(ping); + + assertEquals(pong.getFrom(), ping.getTo()); + assertEquals(pong.getTo(), ping.getFrom()); + assertEquals(pong.getPacketID(), ping.getPacketID()); + } + +}