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 819d259a9..af15fc6de 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 93c7e3df4..dbbd7a4a3 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 @@ -359,6 +359,23 @@ public abstract class Stanza implements TopLevelStreamElement { } } + /** + * 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-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java b/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java index ab2ac3ea8..1365c195b 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 @@ -201,7 +201,10 @@ public abstract class SASLMechanism implements Comparable { private final void authenticate() throws SmackException, NotConnectedException, InterruptedException { 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, @@ -215,7 +218,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 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 8f17befa7..57c69bc1a 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 @@ -30,7 +30,7 @@ import java.util.Set; 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; @@ -89,7 +89,7 @@ import org.jxmpp.jid.Jid; * * @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 @@ -101,22 +101,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); - } - - }); } }); @@ -131,9 +115,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 @@ -188,7 +169,6 @@ public final class Socks5BytestreamManager implements BytestreamManager { if (manager == null) { manager = new Socks5BytestreamManager(connection); managers.put(connection, manager); - manager.activate(); } return manager; } @@ -199,8 +179,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(); } /** @@ -285,7 +266,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); @@ -302,7 +283,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) { @@ -310,7 +291,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) { @@ -426,7 +407,7 @@ public final class Socks5BytestreamManager implements BytestreamManager { */ public Socks5BytestreamSession establishSession(Jid targetJID, String sessionID) throws IOException, InterruptedException, NoResponseException, SmackException, XMPPException{ - + XMPPConnection connection = connection(); XMPPErrorException discoveryException = null; // check if target supports SOCKS5 Bytestream if (!supportsSocks5(targetJID)) { @@ -455,7 +436,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) { @@ -496,7 +477,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()); @@ -506,7 +487,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) { @@ -533,7 +514,7 @@ public final class Socks5BytestreamManager implements BytestreamManager { * @throws InterruptedException */ private boolean supportsSocks5(Jid targetJID) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { - return ServiceDiscoveryManager.getInstanceFor(connection).supportsFeature(targetJID, Bytestream.NAMESPACE); + return ServiceDiscoveryManager.getInstanceFor(connection()).supportsFeature(targetJID, Bytestream.NAMESPACE); } /** @@ -547,12 +528,13 @@ public final class Socks5BytestreamManager implements BytestreamManager { * @throws InterruptedException */ private List determineProxies() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { - 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.getXMPPServiceDomain()); + DiscoverItems discoverItems = serviceDiscoveryManager.discoverItems(connection.getXMPPServiceDomain()); // query all items if they are SOCKS5 proxies for (Item item : discoverItems.getItems()) { @@ -595,6 +577,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 @@ -641,7 +624,7 @@ public final class Socks5BytestreamManager implements BytestreamManager { * is not running */ private List getLocalStreamHost() { - + XMPPConnection connection = connection(); // get local proxy singleton Socks5Proxy socks5Server = Socks5Proxy.getSocks5Proxy(); @@ -710,7 +693,7 @@ public final class Socks5BytestreamManager implements BytestreamManager { protected void replyRejectPacket(IQ packet) throws NotConnectedException, InterruptedException { XMPPError xmppError = new XMPPError(XMPPError.Condition.not_acceptable); IQ errorIQ = IQ.createErrorResponse(packet, xmppError); - this.connection.sendStanza(errorIQ); + connection().sendStanza(errorIQ); } /** @@ -719,7 +702,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(); @@ -729,7 +712,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); } @@ -751,7 +734,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 92d5992ae..fa79ffd59 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; @@ -42,7 +43,7 @@ import org.jxmpp.jid.Jid; 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; @@ -63,7 +64,7 @@ class Socks5ClientForInitiator extends Socks5Client { public Socks5ClientForInitiator(StreamHost streamHost, String digest, XMPPConnection connection, String sessionID, Jid target) { super(streamHost, digest); - this.connection = connection; + this.connection = new WeakReference<>(connection); this.sessionID = sessionID; this.target = target; } @@ -73,7 +74,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) { @@ -112,7 +113,7 @@ class Socks5ClientForInitiator extends Socks5Client { private void activate() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { Bytestream activate = createStreamHostActivation(); // if activation fails #nextResultOrThrow() throws an exception - connection.createPacketCollectorAndSend(activate).nextResultOrThrow(); + connection.get().createPacketCollectorAndSend(activate).nextResultOrThrow(); } /** 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 267c3efb4..a8e581a22 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; @@ -96,7 +97,6 @@ public final 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 @@ -264,7 +264,7 @@ public final 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. @@ -293,7 +293,7 @@ public final class EntityCapsManager extends Manager { // Reset presenceSend when the connection was not resumed if (!resumed) { - presenceSend = false; + presenceSend = null; } } private void processCapsStreamFeatureIfAvailable(XMPPConnection connection) { @@ -341,23 +341,26 @@ public final 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); @@ -506,15 +509,14 @@ public final 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 (InterruptedException | NotConnectedException e) { LOGGER.log(Level.WARNING, "Could could not update presence with caps info", e); 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 70f02f364..a8d24c176 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 @@ -1075,6 +1075,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: