rudimentary bind 2 implementation

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/parser/MessageParser.java |   6 
src/main/java/eu/siacs/conversations/xml/Namespace.java        |   3 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java  | 116 +++
3 files changed, 94 insertions(+), 31 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/parser/MessageParser.java πŸ”—

@@ -313,7 +313,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
     private boolean handleErrorMessage(final Account account, final MessagePacket packet) {
         if (packet.getType() == MessagePacket.TYPE_ERROR) {
             if (packet.fromServer(account)) {
-                final Pair<MessagePacket, Long> forwarded = packet.getForwardedMessagePacket("received", "urn:xmpp:carbons:2");
+                final Pair<MessagePacket, Long> forwarded = packet.getForwardedMessagePacket("received", Namespace.CARBONS);
                 if (forwarded != null) {
                     return handleErrorMessage(account, forwarded.first);
                 }
@@ -389,8 +389,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
             return;
         } else if (original.fromServer(account)) {
             Pair<MessagePacket, Long> f;
-            f = original.getForwardedMessagePacket("received", "urn:xmpp:carbons:2");
-            f = f == null ? original.getForwardedMessagePacket("sent", "urn:xmpp:carbons:2") : f;
+            f = original.getForwardedMessagePacket("received", Namespace.CARBONS);
+            f = f == null ? original.getForwardedMessagePacket("sent", Namespace.CARBONS) : f;
             packet = f != null ? f.first : original;
             if (handleErrorMessage(account, packet)) {
                 return;

src/main/java/eu/siacs/conversations/xml/Namespace.java πŸ”—

@@ -25,9 +25,10 @@ public final class Namespace {
     public static final String NICK = "http://jabber.org/protocol/nick";
     public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = "http://jabber.org/protocol/offline";
     public static final String BIND = "urn:ietf:params:xml:ns:xmpp-bind";
-    public static final String BIND2 = "urn:xmpp:bind2:0";
+    public static final String BIND2 = "urn:xmpp:bind2:1";
     public static final String STREAM_MANAGEMENT = "urn:xmpp:sm:3";
     public static final String CSI = "urn:xmpp:csi:0";
+    public static final String CARBONS = "urn:xmpp:carbons:2";
     public static final String BOOKMARKS_CONVERSION = "urn:xmpp:bookmarks-conversion:0";
     public static final String BOOKMARKS = "storage:bookmarks";
     public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0";

src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java πŸ”—

@@ -13,6 +13,7 @@ import android.util.SparseArray;
 import androidx.annotation.NonNull;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.Collections2;
 
 import org.xmlpull.v1.XmlPullParserException;
 
@@ -32,6 +33,7 @@ import java.security.PrivateKey;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -274,7 +276,7 @@ public class XmppConnection implements Runnable {
         this.attempt++;
         this.verifiedHostname =
                 null; // will be set if user entered hostname is being used or hostname was verified
-                      // with dnssec
+        // with dnssec
         try {
             Socket localSocket;
             shouldAuthenticate = !account.isOptionSet(Account.OPTION_REGISTER);
@@ -409,7 +411,7 @@ public class XmppConnection implements Runnable {
                         if (startXmpp(localSocket)) {
                             localSocket.setSoTimeout(
                                     0); // reset to 0; once the connection is established we don’t
-                                        // want this
+                            // want this
                             if (!hardcoded && !result.equals(storedBackupResult)) {
                                 mXmppConnectionService.databaseBackend.saveResolverResult(
                                         domain, result);
@@ -615,24 +617,9 @@ public class XmppConnection implements Runnable {
                     throw new StateChangingException(Account.State.UNAUTHORIZED);
                 }
                 tagWriter.writeElement(response);
-            } else if (nextTag.isStart("enabled")) {
+            } else if (nextTag.isStart("enabled", Namespace.STREAM_MANAGEMENT)) {
                 final Element enabled = tagReader.readElement(nextTag);
-                if (enabled.getAttributeAsBoolean("resume")) {
-                    this.streamId = enabled.getAttribute("id");
-                    Log.d(
-                            Config.LOGTAG,
-                            account.getJid().asBareJid().toString()
-                                    + ": stream management enabled (resumable)");
-                } else {
-                    Log.d(
-                            Config.LOGTAG,
-                            account.getJid().asBareJid().toString()
-                                    + ": stream management enabled");
-                }
-                this.stanzasReceived = 0;
-                this.inSmacksSession = true;
-                final RequestPacket r = new RequestPacket();
-                tagWriter.writeStanzaAsync(r);
+                processEnabled(enabled);
             } else if (nextTag.isStart("resumed")) {
                 final Element resumed = tagReader.readElement(nextTag);
                 processResumed(resumed);
@@ -771,13 +758,31 @@ public class XmppConnection implements Runnable {
                                 + ": jid changed during SASL 2.0. updating database");
                 mXmppConnectionService.databaseBackend.updateAccount(account);
             }
+            final Element bound = success.findChild("bound", Namespace.BIND2);
             final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3");
             final Element failed = success.findChild("failed", "urn:xmpp:sm:3");
+            // TODO check if resumed and bound exist and throw bind failure
             if (resumed != null && streamId != null) {
                 processResumed(resumed);
             } else if (failed != null) {
                 processFailed(failed, false); // wait for new stream features
             }
+            if (bound != null) {
+                this.isBound = true;
+                final Element streamManagementEnabled =
+                        bound.findChild("enabled", Namespace.STREAM_MANAGEMENT);
+                final Element carbonsEnabled = bound.findChild("enabled", Namespace.CARBONS);
+                if (streamManagementEnabled != null) {
+                    processEnabled(streamManagementEnabled);
+                }
+                if (carbonsEnabled != null) {
+                    Log.d(
+                            Config.LOGTAG,
+                            account.getJid().asBareJid() + ": successfully enabled carbons");
+                    features.carbonsEnabled = true;
+                }
+                sendPostBindInitialization(streamManagementEnabled != null, carbonsEnabled != null);
+            }
         }
         if (version == SaslMechanism.Version.SASL) {
             tagReader.reset();
@@ -794,6 +799,27 @@ public class XmppConnection implements Runnable {
         }
     }
 
+    private void processEnabled(final Element enabled) {
+        final String streamId;
+        if (enabled.getAttributeAsBoolean("resume")) {
+            streamId = enabled.getAttribute("id");
+            Log.d(
+                    Config.LOGTAG,
+                    account.getJid().asBareJid().toString()
+                            + ": stream management enabled (resumable)");
+        } else {
+            Log.d(
+                    Config.LOGTAG,
+                    account.getJid().asBareJid().toString() + ": stream management enabled");
+            streamId = null;
+        }
+        this.streamId = streamId;
+        this.stanzasReceived = 0;
+        this.inSmacksSession = true;
+        final RequestPacket r = new RequestPacket();
+        // tagWriter.writeStanzaAsync(r);
+    }
+
     private void processResumed(final Element resumed) throws StateChangingException {
         this.inSmacksSession = true;
         this.isBound = true;
@@ -1241,6 +1267,16 @@ public class XmppConnection implements Runnable {
             final boolean inlineStreamManagement =
                     inline != null && inline.hasChild("sm", "urn:xmpp:sm:3");
             final boolean inlineBind2 = inline != null && inline.hasChild("bind", Namespace.BIND2);
+            final Element inlineBindFeatures =
+                    this.streamFeatures.findChild("inline", Namespace.BIND2);
+            if (inlineBind2 && inlineBindFeatures != null) {
+                final Element bind =
+                        generateBindRequest(
+                                Collections2.transform(
+                                        inlineBindFeatures.getChildren(),
+                                        c -> c == null ? null : c.getAttribute("var")));
+                authenticate.addChild(bind);
+            }
             if (inlineStreamManagement && streamId != null) {
                 final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived);
                 this.mSmCatchupMessageCounter.set(0);
@@ -1259,9 +1295,26 @@ public class XmppConnection implements Runnable {
                         + "/"
                         + saslMechanism.getMechanism());
         authenticate.setAttribute("mechanism", saslMechanism.getMechanism());
+        Log.d(Config.LOGTAG, "authenticate " + authenticate);
         tagWriter.writeElement(authenticate);
     }
 
+    private Element generateBindRequest(final Collection<String> bindFeatures) {
+        Log.d(Config.LOGTAG, "inline bind features: " + bindFeatures);
+        final Element bind = new Element("bind", Namespace.BIND2);
+        final Element clientId = bind.addChild("client-id");
+        clientId.setAttribute("tag", mXmppConnectionService.getString(R.string.app_name));
+        clientId.setContent(account.getUuid());
+        final Element features = bind.addChild("features");
+        if (bindFeatures.contains(Namespace.CARBONS)) {
+            features.addChild("enable", Namespace.CARBONS);
+        }
+        if (bindFeatures.contains(Namespace.STREAM_MANAGEMENT)) {
+            features.addChild("enable", Namespace.STREAM_MANAGEMENT);
+        }
+        return bind;
+    }
+
     private static List<String> extractMechanisms(final Element stream) {
         final ArrayList<String> mechanisms = new ArrayList<>(stream.getChildren().size());
         for (final Element child : stream.getChildren()) {
@@ -1469,7 +1522,8 @@ public class XmppConnection implements Runnable {
                                                 .hasChild("optional")) {
                                     sendStartSession();
                                 } else {
-                                    sendPostBindInitialization();
+                                    final boolean waitForDisco = enableStreamManagement();
+                                    sendPostBindInitialization(waitForDisco, false);
                                 }
                                 return;
                             } catch (final IllegalArgumentException e) {
@@ -1565,7 +1619,8 @@ public class XmppConnection implements Runnable {
                 startSession,
                 (account, packet) -> {
                     if (packet.getType() == IqPacket.TYPE.RESULT) {
-                        sendPostBindInitialization();
+                        final boolean waitForDisco = enableStreamManagement();
+                        sendPostBindInitialization(waitForDisco, false);
                     } else if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
                         throw new StateChangingError(Account.State.SESSION_FAILURE);
                     }
@@ -1573,7 +1628,7 @@ public class XmppConnection implements Runnable {
                 true);
     }
 
-    private void sendPostBindInitialization() {
+    private boolean enableStreamManagement() {
         final boolean streamManagement =
                 this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT);
         if (streamManagement) {
@@ -1583,15 +1638,22 @@ public class XmppConnection implements Runnable {
                 stanzasSent = 0;
                 mStanzaQueue.clear();
             }
+            return true;
+        } else {
+            return false;
         }
-        features.carbonsEnabled = false;
+    }
+
+    private void sendPostBindInitialization(
+            final boolean waitForDisco, final boolean carbonsEnabled) {
+        features.carbonsEnabled = carbonsEnabled;
         features.blockListRequested = false;
         synchronized (this.disco) {
             this.disco.clear();
         }
         Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery");
         mPendingServiceDiscoveries.set(0);
-        if (!streamManagement
+        if (!waitForDisco
                 || Patches.DISCO_EXCEPTIONS.contains(
                         account.getJid().getDomain().toEscapedString())) {
             Log.d(
@@ -1819,11 +1881,11 @@ public class XmppConnection implements Runnable {
 
     private void sendEnableCarbons() {
         final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
-        iq.addChild("enable", "urn:xmpp:carbons:2");
+        iq.addChild("enable", Namespace.CARBONS);
         this.sendIqPacket(
                 iq,
                 (account, packet) -> {
-                    if (!packet.hasChild("error")) {
+                    if (packet.getType() == IqPacket.TYPE.RESULT) {
                         Log.d(
                                 Config.LOGTAG,
                                 account.getJid().asBareJid() + ": successfully enabled carbons");
@@ -2309,7 +2371,7 @@ public class XmppConnection implements Runnable {
         }
 
         public boolean carbons() {
-            return hasDiscoFeature(account.getDomain(), "urn:xmpp:carbons:2");
+            return hasDiscoFeature(account.getDomain(), Namespace.CARBONS);
         }
 
         public boolean commands() {