implement see-other-host stream error

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/entities/Account.java                |  3 
src/main/java/eu/siacs/conversations/utils/IP.java                        | 12 
src/main/java/eu/siacs/conversations/utils/Resolver.java                  | 62 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java             | 27 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java |  4 
src/main/res/values/strings.xml                                           |  1 
6 files changed, 105 insertions(+), 4 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/entities/Account.java 🔗

@@ -787,6 +787,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
         BIND_FAILURE,
         HOST_UNKNOWN,
         STREAM_ERROR,
+        SEE_OTHER_HOST,
         STREAM_OPENING_ERROR,
         POLICY_VIOLATION,
         PAYMENT_REQUIRED,
@@ -874,6 +875,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
                     return R.string.account_status_stream_opening_error;
                 case PAYMENT_REQUIRED:
                     return R.string.payment_required;
+                case SEE_OTHER_HOST:
+                    return R.string.reconnect_on_other_host;
                 case MISSING_INTERNET_PERMISSION:
                     return R.string.missing_internet_permission;
                 case TEMPORARY_AUTH_FAILURE:

src/main/java/eu/siacs/conversations/utils/IP.java 🔗

@@ -1,5 +1,7 @@
 package eu.siacs.conversations.utils;
 
+import com.google.common.net.InetAddresses;
+
 import java.util.regex.Pattern;
 
 public class IP {
@@ -27,4 +29,14 @@ public class IP {
         }
     }
 
+    public static String unwrapIPv6(final String host) {
+        if (host.length() > 2 && host.charAt(0) == '[' && host.charAt(host.length() - 1) == ']') {
+            final String ip = host.substring(1,host.length() -1);
+            if (InetAddresses.isInetAddress(ip)) {
+                return ip;
+            }
+        }
+        return host;
+    }
+
 }

src/main/java/eu/siacs/conversations/utils/Resolver.java 🔗

@@ -6,7 +6,10 @@ import android.util.Log;
 
 import androidx.annotation.NonNull;
 
+import com.google.common.base.Strings;
 import com.google.common.base.Throwables;
+import com.google.common.net.InetAddresses;
+import com.google.common.primitives.Ints;
 
 import java.io.IOException;
 import java.lang.reflect.Field;
@@ -446,6 +449,65 @@ public class Resolver {
             contentValues.put(AUTHENTICATED, authenticated ? 1 : 0);
             return contentValues;
         }
+
+        public Result seeOtherHost(final String seeOtherHost) {
+            final String hostname = seeOtherHost.trim();
+            if (hostname.isEmpty()) {
+                return null;
+            }
+            final Result result = new Result();
+            result.directTls = this.directTls;
+            final int portSegmentStart = hostname.lastIndexOf(':');
+            if (hostname.charAt(hostname.length() - 1) != ']'
+                    && portSegmentStart >= 0
+                    && hostname.length() >= portSegmentStart + 1) {
+                final String hostPart = hostname.substring(0, portSegmentStart);
+                final String portPart = hostname.substring(portSegmentStart + 1);
+                final Integer port = Ints.tryParse(portPart);
+                if (port == null || Strings.isNullOrEmpty(hostPart)) {
+                    return null;
+                }
+                final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostPart);
+                result.port = port;
+                if (InetAddresses.isInetAddress(host)) {
+                    final InetAddress inetAddress;
+                    try {
+                        inetAddress = InetAddresses.forString(host);
+                    } catch (final IllegalArgumentException e) {
+                        return null;
+                    }
+                    result.ip = inetAddress;
+                } else {
+                    if (hostPart.trim().isEmpty()) {
+                        return null;
+                    }
+                    try {
+                        result.hostname = DNSName.from(hostPart.trim());
+                    } catch (final Exception e) {
+                        return null;
+                    }
+                }
+            } else {
+                final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostname);
+                if (InetAddresses.isInetAddress(host)) {
+                    final InetAddress inetAddress;
+                    try {
+                        inetAddress = InetAddresses.forString(host);
+                    } catch (final IllegalArgumentException e) {
+                        return null;
+                    }
+                    result.ip = inetAddress;
+                } else {
+                    try {
+                        result.hostname = DNSName.from(hostname);
+                    } catch (final Exception e) {
+                        return null;
+                    }
+                }
+                result.port = port;
+            }
+            return result;
+        }
     }
 
 }

src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java 🔗

@@ -189,6 +189,8 @@ public class XmppConnection implements Runnable {
     private HashedToken.Mechanism hashTokenRequest;
     private HttpUrl redirectionUrl = null;
     private String verifiedHostname = null;
+    private Resolver.Result currentResolverResult;
+    private Resolver.Result seeOtherHostResolverResult;
     private volatile Thread mThread;
     private CountDownLatch mStreamCountDownLatch;
 
@@ -360,7 +362,12 @@ public class XmppConnection implements Runnable {
                                         + storedBackupResult);
                     }
                 }
-                for (Iterator<Resolver.Result> iterator = results.iterator();
+                final Resolver.Result seeOtherHost = this.seeOtherHostResolverResult;
+                if (seeOtherHost != null) {
+                    Log.d(Config.LOGTAG,account.getJid().asBareJid()+": injected see-other-host on position 0");
+                    results.add(0, seeOtherHost);
+                }
+                for (final Iterator<Resolver.Result> iterator = results.iterator();
                         iterator.hasNext(); ) {
                     final Resolver.Result result = iterator.next();
                     if (Thread.currentThread().isInterrupted()) {
@@ -374,7 +381,6 @@ public class XmppConnection implements Runnable {
                         features.encryptionEnabled = result.isDirectTls();
                         verifiedHostname =
                                 result.isAuthenticated() ? result.getHostname().toString() : null;
-                        Log.d(Config.LOGTAG, "verified hostname " + verifiedHostname);
                         final InetSocketAddress addr;
                         if (result.getIp() != null) {
                             addr = new InetSocketAddress(result.getIp(), result.getPort());
@@ -422,6 +428,8 @@ public class XmppConnection implements Runnable {
                                 mXmppConnectionService.databaseBackend.saveResolverResult(
                                         domain, result);
                             }
+                            this.currentResolverResult = result;
+                            this.seeOtherHostResolverResult = null;
                             break; // successfully connected to server that speaks xmpp
                         } else {
                             FileBackend.close(localSocket);
@@ -2166,6 +2174,21 @@ public class XmppConnection implements Runnable {
             Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": policy violation. " + text);
             failPendingMessages(text);
             throw new StateChangingException(Account.State.POLICY_VIOLATION);
+        } else if (streamError.hasChild("see-other-host")) {
+            final String seeOtherHost = streamError.findChildContent("see-other-host");
+            final Resolver.Result currentResolverResult = this.currentResolverResult;
+            if (Strings.isNullOrEmpty(seeOtherHost) || currentResolverResult == null) {
+                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError);
+                throw new StateChangingException(Account.State.STREAM_ERROR);
+            }
+            Log.d(Config.LOGTAG,account.getJid().asBareJid()+": see other host: "+seeOtherHost+" "+currentResolverResult);
+            final Resolver.Result seeOtherResult = currentResolverResult.seeOtherHost(seeOtherHost);
+            if (seeOtherResult != null) {
+                this.seeOtherHostResolverResult = seeOtherResult;
+                throw new StateChangingException(Account.State.SEE_OTHER_HOST);
+            } else {
+                throw new StateChangingException(Account.State.STREAM_ERROR);
+            }
         } else {
             Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError);
             throw new StateChangingException(Account.State.STREAM_ERROR);

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java 🔗

@@ -553,13 +553,13 @@ public class JingleRtpConnection extends AbstractJingleConnection
             sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage());
             return;
         }
-        processCandidates(receivedContentAccept.contents.entrySet());
-        updateEndUserState();
         Log.d(
                 Config.LOGTAG,
                 id.getAccount().getJid().asBareJid()
                         + ": remote has accepted content-add "
                         + ContentAddition.summary(receivedContentAccept));
+        processCandidates(receivedContentAccept.contents.entrySet());
+        updateEndUserState();
     }
 
     private void receiveContentModify(final JinglePacket jinglePacket) {

src/main/res/values/strings.xml 🔗

@@ -587,6 +587,7 @@
     <string name="type_web">Web browser</string>
     <string name="type_console">Console</string>
     <string name="payment_required">Payment required</string>
+    <string name="reconnect_on_other_host">Reconnect on other host</string>
     <string name="missing_internet_permission">Grant permission to use the Internet</string>
     <string name="me">Me</string>
     <string name="contact_asks_for_presence_subscription">Contact asks for presence subscription</string>