From d97b115170add8dc16fc0ed4b5438db0c59d45fa Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Sat, 9 May 2015 15:43:14 +0200 Subject: [PATCH 1/5] Smack 4.1.2-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 7e83af347..184deec47 100644 --- a/version.gradle +++ b/version.gradle @@ -1,7 +1,7 @@ allprojects { ext { - shortVersion = '4.1.1' - isSnapshot = false + shortVersion = '4.1.2' + isSnapshot = true jxmppVersion = '0.4.2-beta1' smackMinAndroidSdk = 8 } From 576980097ec463fdc1cc8fcf0e398b1f9ab65adf Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Tue, 19 May 2015 14:21:52 +0200 Subject: [PATCH 2/5] Request SM ack when re-sending after stream resumption Fixes SMACK-667. --- .../java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java index 378f3299d..64de91db5 100644 --- a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java @@ -1117,6 +1117,12 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { } smResumedSyncPoint.reportSuccess(); smEnabledSyncPoint.reportSuccess(); + // If there where stanzas resent, then request a SM ack for them. + // Writer's sendStreamElement() won't do it automatically based on + // predicates. + if (!stanzasToResend.isEmpty()) { + requestSmAcknowledgementInternal(); + } LOGGER.fine("Stream Management (XEP-198): Stream resumed"); break; case AckAnswer.ELEMENT: From c120bc1cbcc67dff1601dad0f9773a5936c3e30f Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Thu, 21 May 2015 22:20:30 +0200 Subject: [PATCH 3/5] Handle empty array in SASLMechanism.authenticate() like null array. Thanks to Anthony Sorvari for reporting. Fixes SMACK-670. --- .../java/org/jivesoftware/smack/sasl/SASLMechanism.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java b/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java index eba0d7045..3ac5a685b 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java @@ -195,7 +195,10 @@ public abstract class SASLMechanism implements Comparable { private final void authenticate() throws SmackException, NotConnectedException { byte[] authenticationBytes = getAuthenticationText(); String authenticationText; - if (authenticationBytes != null) { + // Some SASL mechanisms do return an empty array (e.g. EXTERNAL from javax), so check that + // the array is not-empty. Mechanisms are allowed to return either 'null' or an empty array + // if there is no authentication text. + if (authenticationBytes != null && authenticationBytes.length > 0) { authenticationText = Base64.encodeToString(authenticationBytes); } else { // RFC6120 6.4.2 "If the initiating entity needs to send a zero-length initial response, @@ -209,7 +212,8 @@ public abstract class SASLMechanism implements Comparable { /** * Should return the initial response of the SASL mechanism. The returned byte array will be - * send base64 encoded to the server. SASL mechanism are free to return null here. + * send base64 encoded to the server. SASL mechanism are free to return null or an + * empty array here. * * @return the initial response or null * @throws SmackException From 385798f9baff0d9e3bfd98807f95690361f93c84 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Thu, 21 May 2015 22:04:26 +0200 Subject: [PATCH 4/5] Only add Entity Capabilities extension to available presences Also don't override eventually send presences on updateLocalEntityCaps(), instead save the last sent Presence stanza and re-send that stanza. SMACK-669. --- .../jivesoftware/smack/packet/Presence.java | 13 ++++++++++ .../org/jivesoftware/smack/packet/Stanza.java | 17 ++++++++++++ .../smackx/caps/EntityCapsManager.java | 26 ++++++++++--------- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/Presence.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/Presence.java index 5781e94c0..e596e53ea 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/Presence.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/Presence.java @@ -19,6 +19,7 @@ package org.jivesoftware.smack.packet; import java.util.Locale; +import org.jivesoftware.smack.packet.id.StanzaIdUtil; import org.jivesoftware.smack.util.Objects; import org.jivesoftware.smack.util.TypedCloneable; import org.jivesoftware.smack.util.XmlStringBuilder; @@ -259,6 +260,18 @@ public final class Presence extends Stanza implements TypedCloneable { return new Presence(this); } + /** + * Clone this presence and set a newly generated stanza ID as the clone's ID. + * + * @return a "clone" of this presence with a different stanza ID. + * @since 4.1.2 + */ + public Presence cloneWithNewId() { + Presence clone = clone(); + clone.setStanzaId(StanzaIdUtil.newStanzaId()); + return clone; + } + /** * An enum to represent the presence type. Note that presence type is often confused * with presence mode. Generally, if a user is signed in to a server, they have a presence diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/Stanza.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/Stanza.java index d256b2d3f..c13f831e3 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/Stanza.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/Stanza.java @@ -316,6 +316,23 @@ public abstract class Stanza implements TopLevelStreamElement, Packet { } } + /** + * Add the given extension and override eventually existing extensions with the same name and + * namespace. + * + * @param extension the extension element to add. + * @return one of the removed extensions or null if there are none. + * @since 4.1.2 + */ + public ExtensionElement overrideExtension(ExtensionElement extension) { + if (extension == null) return null; + synchronized (packetExtensions) { + ExtensionElement removedExtension = removeExtension(extension); + addExtension(extension); + return removedExtension; + } + } + /** * Adds a collection of stanza(/packet) extensions to the packet. Does nothing if extensions is null. * diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/caps/EntityCapsManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/caps/EntityCapsManager.java index 5bd2880fd..0818b7b87 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/caps/EntityCapsManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/caps/EntityCapsManager.java @@ -30,6 +30,7 @@ import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.filter.NotFilter; +import org.jivesoftware.smack.filter.PresenceTypeFilter; import org.jivesoftware.smack.filter.StanzaFilter; import org.jivesoftware.smack.filter.AndFilter; import org.jivesoftware.smack.filter.StanzaTypeFilter; @@ -94,7 +95,6 @@ public class EntityCapsManager extends Manager { ELEMENT, NAMESPACE)); private static final StanzaFilter PRESENCES_WITHOUT_CAPS = new AndFilter(new StanzaTypeFilter(Presence.class), new NotFilter(new StanzaExtensionFilter( ELEMENT, NAMESPACE))); - private static final StanzaFilter PRESENCES = StanzaTypeFilter.PRESENCE; /** * Map of "node + '#' + hash" to DiscoverInfo data @@ -262,7 +262,7 @@ public class EntityCapsManager extends Manager { private boolean entityCapsEnabled; private CapsVersionAndHash currentCapsVersion; - private boolean presenceSend = false; + private volatile Presence presenceSend; /** * The entity node String used by this EntityCapsManager instance. @@ -291,7 +291,7 @@ public class EntityCapsManager extends Manager { // Reset presenceSend when the connection was not resumed if (!resumed) { - presenceSend = false; + presenceSend = null; } } private void processCapsStreamFeatureIfAvailable(XMPPConnection connection) { @@ -339,23 +339,26 @@ public class EntityCapsManager extends Manager { connection.addPacketSendingListener(new StanzaListener() { @Override public void processPacket(Stanza packet) { - presenceSend = true; + presenceSend = (Presence) packet; } - }, PRESENCES); + }, PresenceTypeFilter.AVAILABLE); // Intercept presence packages and add caps data when intended. // XEP-0115 specifies that a client SHOULD include entity capabilities // with every presence notification it sends. StanzaListener packetInterceptor = new StanzaListener() { public void processPacket(Stanza packet) { - if (!entityCapsEnabled) + if (!entityCapsEnabled) { + // Be sure to not send stanzas with the caps extension if it's not enabled + packet.removeExtension(CapsExtension.ELEMENT, CapsExtension.NAMESPACE); return; + } CapsVersionAndHash capsVersionAndHash = getCapsVersionAndHash(); CapsExtension caps = new CapsExtension(entityNode, capsVersionAndHash.version, capsVersionAndHash.hash); - packet.addExtension(caps); + packet.overrideExtension(caps); } }; - connection.addPacketInterceptor(packetInterceptor, PRESENCES); + connection.addPacketInterceptor(packetInterceptor, PresenceTypeFilter.AVAILABLE); // It's important to do this as last action. Since it changes the // behavior of the SDM in some ways sdm.setEntityCapsManager(this); @@ -502,15 +505,14 @@ public class EntityCapsManager extends Manager { } }); - // Send an empty presence, and let the packet interceptor + // Re-send the last sent presence, and let the stanza interceptor // add a node to it. // See http://xmpp.org/extensions/xep-0115.html#advertise // We only send a presence packet if there was already one send // to respect ConnectionConfiguration.isSendPresence() - if (connection != null && connection.isAuthenticated() && presenceSend) { - Presence presence = new Presence(Presence.Type.available); + if (connection != null && connection.isAuthenticated() && presenceSend != null) { try { - connection.sendStanza(presence); + connection.sendStanza(presenceSend.cloneWithNewId()); } catch (NotConnectedException e) { LOGGER.log(Level.WARNING, "Could could not update presence with caps info", e); From 35317a19bd373c3078ca249411de6668c295275e Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Sun, 24 May 2015 17:18:48 +0200 Subject: [PATCH 5/5] Remove ConnectionListener from Socks5BytestreamManager and use weak references. Disabling the Socks5Manager every time the connection is terminated, and re-enabling it when it got connected again causes unwanted side effects. Like adding a new feature to the ServiceDiscoveryManager causes an update of the entity's capabilities, which then triggers a new outgoing presence (announcing the new caps version). SMACK-671 --- .../socks5/Socks5BytestreamManager.java | 59 +++++++------------ .../socks5/Socks5ClientForInitiator.java | 9 +-- 2 files changed, 26 insertions(+), 42 deletions(-) diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamManager.java index 4145351b5..04bd3d9fe 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamManager.java @@ -28,7 +28,7 @@ import java.util.Random; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeoutException; -import org.jivesoftware.smack.AbstractConnectionClosedListener; +import org.jivesoftware.smack.Manager; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.FeatureNotSupportedException; @@ -86,7 +86,7 @@ import org.jivesoftware.smackx.filetransfer.FileTransferManager; * * @author Henning Staib */ -public final class Socks5BytestreamManager implements BytestreamManager { +public final class Socks5BytestreamManager extends Manager implements BytestreamManager { /* * create a new Socks5BytestreamManager and register a shutdown listener on every established @@ -98,22 +98,6 @@ public final class Socks5BytestreamManager implements BytestreamManager { public void connectionCreated(final XMPPConnection connection) { // create the manager for this connection Socks5BytestreamManager.getBytestreamManager(connection); - - // register shutdown listener - connection.addConnectionListener(new AbstractConnectionClosedListener() { - - @Override - public void connectionTerminated() { - Socks5BytestreamManager.getBytestreamManager(connection).disableService(); - } - - @Override - public void reconnectionSuccessful() { - // re-create the manager for this connection - Socks5BytestreamManager.getBytestreamManager(connection); - } - - }); } }); @@ -128,9 +112,6 @@ public final class Socks5BytestreamManager implements BytestreamManager { /* stores one Socks5BytestreamManager for each XMPP connection */ private final static Map managers = new HashMap(); - /* XMPP connection */ - private final XMPPConnection connection; - /* * assigns a user to a listener that is informed if a bytestream request for this user is * received @@ -185,7 +166,6 @@ public final class Socks5BytestreamManager implements BytestreamManager { if (manager == null) { manager = new Socks5BytestreamManager(connection); managers.put(connection, manager); - manager.activate(); } return manager; } @@ -196,8 +176,9 @@ public final class Socks5BytestreamManager implements BytestreamManager { * @param connection the XMPP connection */ private Socks5BytestreamManager(XMPPConnection connection) { - this.connection = connection; + super(connection); this.initiationListener = new InitiationListener(this); + activate(); } /** @@ -282,7 +263,7 @@ public final class Socks5BytestreamManager implements BytestreamManager { * Using the file transfer API will automatically re-enable the SOCKS5 Bytestream feature. */ public synchronized void disableService() { - + XMPPConnection connection = connection(); // remove initiation packet listener connection.unregisterIQRequestHandler(initiationListener); @@ -299,7 +280,7 @@ public final class Socks5BytestreamManager implements BytestreamManager { this.ignoredBytestreamRequests.clear(); // remove manager from static managers map - managers.remove(this.connection); + managers.remove(connection); // shutdown local SOCKS5 proxy if there are no more managers for other connections if (managers.size() == 0) { @@ -307,7 +288,7 @@ public final class Socks5BytestreamManager implements BytestreamManager { } // remove feature from service discovery - ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection); + ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection); // check if service discovery is not already disposed by connection shutdown if (serviceDiscoveryManager != null) { @@ -423,7 +404,7 @@ public final class Socks5BytestreamManager implements BytestreamManager { */ public Socks5BytestreamSession establishSession(String targetJID, String sessionID) throws IOException, InterruptedException, NoResponseException, SmackException, XMPPException{ - + XMPPConnection connection = connection(); XMPPErrorException discoveryException = null; // check if target supports SOCKS5 Bytestream if (!supportsSocks5(targetJID)) { @@ -452,7 +433,7 @@ public final class Socks5BytestreamManager implements BytestreamManager { } // compute digest - String digest = Socks5Utils.createDigest(sessionID, this.connection.getUser(), targetJID); + String digest = Socks5Utils.createDigest(sessionID, connection.getUser(), targetJID); // prioritize last working SOCKS5 proxy if exists if (this.proxyPrioritizationEnabled && this.lastWorkingProxy != null) { @@ -493,7 +474,7 @@ public final class Socks5BytestreamManager implements BytestreamManager { // build SOCKS5 client Socks5Client socks5Client = new Socks5ClientForInitiator(usedStreamHost, digest, - this.connection, sessionID, targetJID); + connection, sessionID, targetJID); // establish connection to proxy Socket socket = socks5Client.getSocket(getProxyConnectionTimeout()); @@ -503,7 +484,7 @@ public final class Socks5BytestreamManager implements BytestreamManager { // negotiation successful, return the output stream return new Socks5BytestreamSession(socket, usedStreamHost.getJID().equals( - this.connection.getUser())); + connection.getUser())); } catch (TimeoutException e) { @@ -529,7 +510,7 @@ public final class Socks5BytestreamManager implements BytestreamManager { * @throws NotConnectedException */ private boolean supportsSocks5(String targetJID) throws NoResponseException, XMPPErrorException, NotConnectedException { - return ServiceDiscoveryManager.getInstanceFor(connection).supportsFeature(targetJID, Bytestream.NAMESPACE); + return ServiceDiscoveryManager.getInstanceFor(connection()).supportsFeature(targetJID, Bytestream.NAMESPACE); } /** @@ -542,12 +523,13 @@ public final class Socks5BytestreamManager implements BytestreamManager { * @throws NotConnectedException */ private List determineProxies() throws NoResponseException, XMPPErrorException, NotConnectedException { - ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection); + XMPPConnection connection = connection(); + ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection); List proxies = new ArrayList(); // get all items from XMPP server - DiscoverItems discoverItems = serviceDiscoveryManager.discoverItems(this.connection.getServiceName()); + DiscoverItems discoverItems = serviceDiscoveryManager.discoverItems(connection.getServiceName()); // query all items if they are SOCKS5 proxies for (Item item : discoverItems.getItems()) { @@ -590,6 +572,7 @@ public final class Socks5BytestreamManager implements BytestreamManager { * @return a list of stream hosts containing the IP address an the port */ private List determineStreamHostInfos(List proxies) { + XMPPConnection connection = connection(); List streamHosts = new ArrayList(); // add local proxy on first position if exists @@ -636,7 +619,7 @@ public final class Socks5BytestreamManager implements BytestreamManager { * is not running */ private List getLocalStreamHost() { - + XMPPConnection connection = connection(); // get local proxy singleton Socks5Proxy socks5Server = Socks5Proxy.getSocks5Proxy(); @@ -704,7 +687,7 @@ public final class Socks5BytestreamManager implements BytestreamManager { protected void replyRejectPacket(IQ packet) throws NotConnectedException { XMPPError xmppError = new XMPPError(XMPPError.Condition.not_acceptable); IQ errorIQ = IQ.createErrorResponse(packet, xmppError); - this.connection.sendStanza(errorIQ); + connection().sendStanza(errorIQ); } /** @@ -713,7 +696,7 @@ public final class Socks5BytestreamManager implements BytestreamManager { */ private void activate() { // register bytestream initiation packet listener - connection.registerIQRequestHandler(initiationListener); + connection().registerIQRequestHandler(initiationListener); // enable SOCKS5 feature enableService(); @@ -723,7 +706,7 @@ public final class Socks5BytestreamManager implements BytestreamManager { * Adds the SOCKS5 Bytestream feature to the service discovery. */ private void enableService() { - ServiceDiscoveryManager manager = ServiceDiscoveryManager.getInstanceFor(this.connection); + ServiceDiscoveryManager manager = ServiceDiscoveryManager.getInstanceFor(connection()); manager.addFeature(Bytestream.NAMESPACE); } @@ -745,7 +728,7 @@ public final class Socks5BytestreamManager implements BytestreamManager { * @return the XMPP connection */ protected XMPPConnection getConnection() { - return this.connection; + return connection(); } /** diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5ClientForInitiator.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5ClientForInitiator.java index 9653d78f8..d0a7c5897 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5ClientForInitiator.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5ClientForInitiator.java @@ -17,6 +17,7 @@ package org.jivesoftware.smackx.bytestreams.socks5; import java.io.IOException; +import java.lang.ref.WeakReference; import java.net.Socket; import java.util.concurrent.TimeoutException; @@ -41,7 +42,7 @@ import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost; class Socks5ClientForInitiator extends Socks5Client { /* the XMPP connection used to communicate with the SOCKS5 proxy */ - private XMPPConnection connection; + private WeakReference connection; /* the session ID used to activate SOCKS5 stream */ private String sessionID; @@ -61,7 +62,7 @@ class Socks5ClientForInitiator extends Socks5Client { public Socks5ClientForInitiator(StreamHost streamHost, String digest, XMPPConnection connection, String sessionID, String target) { super(streamHost, digest); - this.connection = connection; + this.connection = new WeakReference<>(connection); this.sessionID = sessionID; this.target = target; } @@ -71,7 +72,7 @@ class Socks5ClientForInitiator extends Socks5Client { Socket socket = null; // check if stream host is the local SOCKS5 proxy - if (this.streamHost.getJID().equals(this.connection.getUser())) { + if (this.streamHost.getJID().equals(this.connection.get().getUser())) { Socks5Proxy socks5Server = Socks5Proxy.getSocks5Proxy(); socket = socks5Server.getSocket(this.digest); if (socket == null) { @@ -109,7 +110,7 @@ class Socks5ClientForInitiator extends Socks5Client { private void activate() throws NoResponseException, XMPPErrorException, NotConnectedException { Bytestream activate = createStreamHostActivation(); // if activation fails #nextResultOrThrow() throws an exception - connection.createPacketCollectorAndSend(activate).nextResultOrThrow(); + connection.get().createPacketCollectorAndSend(activate).nextResultOrThrow(); } /**