XmppConnection.java

   1package eu.siacs.conversations.xmpp;
   2import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
   3
   4import android.content.Context;
   5import android.graphics.Bitmap;
   6import android.graphics.BitmapFactory;
   7import android.os.Build;
   8import android.os.SystemClock;
   9import android.security.KeyChain;
  10import android.util.Base64;
  11import android.util.Log;
  12import android.util.Pair;
  13import android.util.SparseArray;
  14import androidx.annotation.NonNull;
  15import androidx.annotation.Nullable;
  16
  17import com.google.common.base.MoreObjects;
  18import com.google.common.base.Optional;
  19import com.google.common.base.Preconditions;
  20import com.google.common.base.Strings;
  21import com.google.common.base.Throwables;
  22import com.google.common.collect.ClassToInstanceMap;
  23import com.google.common.collect.ImmutableClassToInstanceMap;
  24import com.google.common.collect.ImmutableList;
  25import com.google.common.collect.Iterables;
  26import com.google.common.primitives.Ints;
  27
  28import org.xmlpull.v1.XmlPullParserException;
  29
  30import java.io.ByteArrayInputStream;
  31import java.io.IOException;
  32import java.io.InputStream;
  33import java.net.ConnectException;
  34import java.net.IDN;
  35import java.net.InetAddress;
  36import java.net.InetSocketAddress;
  37import java.net.Socket;
  38import java.net.UnknownHostException;
  39import java.security.KeyManagementException;
  40import java.security.NoSuchAlgorithmException;
  41import java.security.Principal;
  42import java.security.PrivateKey;
  43import java.security.cert.Certificate;
  44import java.security.cert.X509Certificate;
  45import java.util.ArrayList;
  46import java.util.Arrays;
  47import java.util.Collection;
  48import java.util.Collections;
  49import java.util.HashMap;
  50import java.util.HashSet;
  51import java.util.Hashtable;
  52import java.util.Iterator;
  53import java.util.List;
  54import java.util.Map.Entry;
  55import java.util.Set;
  56import java.util.concurrent.CountDownLatch;
  57import java.util.concurrent.Executors;
  58import java.util.concurrent.ScheduledExecutorService;
  59import java.util.concurrent.ScheduledFuture;
  60import java.util.concurrent.TimeUnit;
  61import java.util.concurrent.atomic.AtomicBoolean;
  62import java.util.concurrent.atomic.AtomicInteger;
  63import java.util.function.Consumer;
  64import java.util.regex.Matcher;
  65
  66import javax.net.ssl.KeyManager;
  67import javax.net.ssl.SSLContext;
  68import javax.net.ssl.SSLPeerUnverifiedException;
  69import javax.net.ssl.SSLSession;
  70import javax.net.ssl.SSLSocket;
  71import javax.net.ssl.SSLSocketFactory;
  72import javax.net.ssl.X509KeyManager;
  73import javax.net.ssl.X509TrustManager;
  74
  75import com.google.common.util.concurrent.FutureCallback;
  76import com.google.common.util.concurrent.Futures;
  77import com.google.common.util.concurrent.ListenableFuture;
  78import com.google.common.util.concurrent.MoreExecutors;
  79import com.google.common.util.concurrent.SettableFuture;
  80import de.gultsch.common.Patterns;
  81import eu.siacs.conversations.AppSettings;
  82import eu.siacs.conversations.BuildConfig;
  83import eu.siacs.conversations.Config;
  84import eu.siacs.conversations.R;
  85import eu.siacs.conversations.crypto.XmppDomainVerifier;
  86import eu.siacs.conversations.crypto.axolotl.AxolotlService;
  87import eu.siacs.conversations.crypto.sasl.ChannelBinding;
  88import eu.siacs.conversations.crypto.sasl.ChannelBindingMechanism;
  89import eu.siacs.conversations.crypto.sasl.DowngradeProtection;
  90import eu.siacs.conversations.crypto.sasl.HashedToken;
  91import eu.siacs.conversations.crypto.sasl.SaslMechanism;
  92import eu.siacs.conversations.crypto.sasl.ScramMechanism;
  93import eu.siacs.conversations.entities.Account;
  94import eu.siacs.conversations.entities.Message;
  95import eu.siacs.conversations.generator.IqGenerator;
  96import eu.siacs.conversations.http.HttpConnectionManager;
  97import eu.siacs.conversations.parser.IqParser;
  98import eu.siacs.conversations.parser.MessageParser;
  99import eu.siacs.conversations.parser.PresenceParser;
 100import eu.siacs.conversations.persistance.DatabaseBackend;
 101import eu.siacs.conversations.persistance.FileBackend;
 102import eu.siacs.conversations.services.MemorizingTrustManager;
 103import eu.siacs.conversations.services.MessageArchiveService;
 104import eu.siacs.conversations.services.NotificationService;
 105import eu.siacs.conversations.services.XmppConnectionService;
 106import eu.siacs.conversations.ui.util.PendingItem;
 107import eu.siacs.conversations.utils.AccountUtils;
 108import eu.siacs.conversations.utils.CryptoHelper;
 109import eu.siacs.conversations.utils.PhoneHelper;
 110import eu.siacs.conversations.utils.Resolver;
 111import eu.siacs.conversations.utils.SSLSockets;
 112import eu.siacs.conversations.utils.SocksSocketFactory;
 113import eu.siacs.conversations.utils.XmlHelper;
 114import eu.siacs.conversations.xml.Element;
 115import eu.siacs.conversations.xml.LocalizedContent;
 116import eu.siacs.conversations.xml.Namespace;
 117import eu.siacs.conversations.xml.Tag;
 118import eu.siacs.conversations.xml.TagWriter;
 119import eu.siacs.conversations.xml.XmlReader;
 120import eu.siacs.conversations.xmpp.bind.Bind2;
 121import eu.siacs.conversations.xmpp.forms.Data;
 122import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
 123import eu.siacs.conversations.xmpp.manager.AbstractManager;
 124import eu.siacs.conversations.xmpp.manager.DiscoManager;
 125import im.conversations.android.xmpp.Entity;
 126import im.conversations.android.xmpp.model.AuthenticationFailure;
 127import im.conversations.android.xmpp.model.AuthenticationRequest;
 128import im.conversations.android.xmpp.model.AuthenticationStreamFeature;
 129import im.conversations.android.xmpp.model.StreamElement;
 130import im.conversations.android.xmpp.model.bind2.Bind;
 131import im.conversations.android.xmpp.model.bind2.Bound;
 132import im.conversations.android.xmpp.model.cb.SaslChannelBinding;
 133import im.conversations.android.xmpp.model.csi.Active;
 134import im.conversations.android.xmpp.model.csi.Inactive;
 135import im.conversations.android.xmpp.model.disco.info.InfoQuery;
 136import im.conversations.android.xmpp.model.error.Condition;
 137import im.conversations.android.xmpp.model.fast.Fast;
 138import im.conversations.android.xmpp.model.fast.RequestToken;
 139import im.conversations.android.xmpp.model.jingle.Jingle;
 140import im.conversations.android.xmpp.model.sasl.Auth;
 141import im.conversations.android.xmpp.model.sasl.Failure;
 142import im.conversations.android.xmpp.model.sasl.Mechanisms;
 143import im.conversations.android.xmpp.model.sasl.Response;
 144import im.conversations.android.xmpp.model.sasl.SaslError;
 145import im.conversations.android.xmpp.model.sasl.Success;
 146import im.conversations.android.xmpp.model.sasl2.Authenticate;
 147import im.conversations.android.xmpp.model.sasl2.Authentication;
 148import im.conversations.android.xmpp.model.sasl2.UserAgent;
 149import im.conversations.android.xmpp.model.sm.Ack;
 150import im.conversations.android.xmpp.model.sm.Enable;
 151import im.conversations.android.xmpp.model.sm.Enabled;
 152import im.conversations.android.xmpp.model.sm.Failed;
 153import im.conversations.android.xmpp.model.sm.Request;
 154import im.conversations.android.xmpp.model.sm.Resume;
 155import im.conversations.android.xmpp.model.sm.Resumed;
 156import im.conversations.android.xmpp.model.sm.StreamManagement;
 157import im.conversations.android.xmpp.model.stanza.Iq;
 158import im.conversations.android.xmpp.model.stanza.Presence;
 159import im.conversations.android.xmpp.model.stanza.Stanza;
 160import im.conversations.android.xmpp.model.streams.StreamError;
 161import im.conversations.android.xmpp.model.tls.Proceed;
 162import im.conversations.android.xmpp.model.tls.StartTls;
 163import im.conversations.android.xmpp.processor.BindProcessor;
 164import java.io.ByteArrayInputStream;
 165import java.io.IOException;
 166import java.io.InputStream;
 167import java.net.ConnectException;
 168import java.net.IDN;
 169import java.net.InetAddress;
 170import java.net.InetSocketAddress;
 171import java.net.Socket;
 172import java.net.UnknownHostException;
 173import java.security.KeyManagementException;
 174import java.security.NoSuchAlgorithmException;
 175import java.security.Principal;
 176import java.security.PrivateKey;
 177import java.security.cert.X509Certificate;
 178import java.util.ArrayList;
 179import java.util.Arrays;
 180import java.util.Collection;
 181import java.util.Collections;
 182import java.util.HashMap;
 183import java.util.HashSet;
 184import java.util.Hashtable;
 185import java.util.Iterator;
 186import java.util.List;
 187import java.util.Map;
 188import java.util.Map.Entry;
 189import java.util.Set;
 190import java.util.concurrent.CountDownLatch;
 191import java.util.concurrent.TimeUnit;
 192import java.util.concurrent.TimeoutException;
 193import java.util.concurrent.atomic.AtomicBoolean;
 194import java.util.concurrent.atomic.AtomicInteger;
 195import java.util.function.Consumer;
 196import java.util.regex.Matcher;
 197import javax.net.ssl.KeyManager;
 198import javax.net.ssl.SSLContext;
 199import javax.net.ssl.SSLPeerUnverifiedException;
 200import javax.net.ssl.SSLSocket;
 201import javax.net.ssl.SSLSocketFactory;
 202import javax.net.ssl.X509KeyManager;
 203import javax.net.ssl.X509TrustManager;
 204import okhttp3.HttpUrl;
 205
 206public class XmppConnection implements Runnable {
 207
 208    protected final Account account;
 209    private final Features features = new Features(this);
 210    private final HashMap<String, Jid> commands = new HashMap<>();
 211    private final SparseArray<Stanza> mStanzaQueue = new SparseArray<>();
 212    private final Hashtable<String, Pair<Iq, Pair<Consumer<Iq>, ScheduledFuture>>> packetCallbacks = new Hashtable<>();
 213    private final Set<OnAdvancedStreamFeaturesLoaded> advancedStreamFeaturesLoadedListeners =
 214            new HashSet<>();
 215    private final AppSettings appSettings;
 216    private final XmppConnectionService mXmppConnectionService;
 217    private Socket socket;
 218    private XmlReader tagReader;
 219    private TagWriter tagWriter = new TagWriter();
 220    private boolean shouldAuthenticate = true;
 221    private boolean inSmacksSession = false;
 222    private boolean quickStartInProgress = false;
 223    private boolean isBound = false;
 224    private boolean offlineMessagesRetrieved = false;
 225    private im.conversations.android.xmpp.model.streams.Features streamFeatures;
 226    private im.conversations.android.xmpp.model.streams.Features boundStreamFeatures;
 227    private StreamId streamId = null;
 228    private int stanzasReceived = 0;
 229    private int stanzasSent = 0;
 230    private int stanzasSentBeforeAuthentication;
 231    private long lastPacketReceived = 0;
 232    private long lastPingSent = 0;
 233    private long lastConnectionStarted = 0;
 234    private long lastSessionStarted = 0;
 235    private long lastDiscoStarted = 0;
 236    private boolean isMamPreferenceAlways = false;
 237    private final AtomicBoolean mWaitForDisco = new AtomicBoolean(true);
 238    private final AtomicBoolean mWaitingForSmCatchup = new AtomicBoolean(false);
 239    private final AtomicInteger mSmCatchupMessageCounter = new AtomicInteger(0);
 240    private boolean mInteractive = false;
 241    private int attempt = 0;
 242    private OnJinglePacketReceived jingleListener = null;
 243
 244    private final Consumer<Presence> presenceListener;
 245    private final Consumer<Iq> unregisteredIqListener;
 246    private final Consumer<im.conversations.android.xmpp.model.stanza.Message> messageListener;
 247    private OnStatusChanged statusListener = null;
 248    private final Runnable bindListener;
 249    private OnMessageAcknowledged acknowledgedListener = null;
 250    private final PendingItem<String> pendingResumeId = new PendingItem<>();
 251    private LoginInfo loginInfo;
 252    private HashedToken.Mechanism hashTokenRequest;
 253    private HttpUrl redirectionUrl = null;
 254    private String verifiedHostname = null;
 255    private Resolver.Result currentResolverResult;
 256    private Resolver.Result seeOtherHostResolverResult;
 257    private volatile Thread mThread;
 258    private CountDownLatch mStreamCountDownLatch;
 259    private static ScheduledExecutorService SCHEDULER = Executors.newScheduledThreadPool(1);
 260    private boolean dane = false;
 261    private final ClassToInstanceMap<AbstractManager> managers;
 262
 263    public XmppConnection(final Account account, final XmppConnectionService service) {
 264        this.account = account;
 265        this.mXmppConnectionService = service;
 266        this.appSettings = mXmppConnectionService.getAppSettings();
 267        this.presenceListener = new PresenceParser(service, account);
 268        this.unregisteredIqListener = new IqParser(service, account);
 269        this.messageListener = new MessageParser(service, account);
 270        this.bindListener = new BindProcessor(service, account);
 271        this.managers =
 272                new ImmutableClassToInstanceMap.Builder<AbstractManager>()
 273                        .put(
 274                                DiscoManager.class,
 275                                new DiscoManager(service, this))
 276                        .build();
 277    }
 278
 279    private static void fixResource(final Context context, final Account account) {
 280        String resource = account.getResource();
 281        int fixedPartLength =
 282                context.getString(R.string.app_name).length() + 1; // include the trailing dot
 283        int randomPartLength = 4; // 3 bytes
 284        if (resource != null && resource.length() > fixedPartLength + randomPartLength) {
 285            if (validBase64(
 286                    resource.substring(fixedPartLength, fixedPartLength + randomPartLength))) {
 287                account.setResource(resource.substring(0, fixedPartLength + randomPartLength));
 288            }
 289        }
 290    }
 291
 292    private static boolean validBase64(final String input) {
 293        try {
 294            return Base64.decode(input, Base64.URL_SAFE).length == 3;
 295        } catch (final Throwable throwable) {
 296            return false;
 297        }
 298    }
 299
 300    public boolean daneVerified() {
 301        return dane;
 302    }
 303
 304    public boolean resolverAuthenticated() {
 305        if (currentResolverResult == null) return false;
 306        return currentResolverResult.isAuthenticated();
 307    }
 308
 309    private void changeStatus(final Account.State nextStatus) {
 310        synchronized (this) {
 311            if (Thread.currentThread().isInterrupted()) {
 312                Log.d(
 313                        Config.LOGTAG,
 314                        account.getJid().asBareJid()
 315                                + ": not changing status to "
 316                                + nextStatus
 317                                + " because thread was interrupted");
 318                return;
 319            }
 320            if (account.getStatus() != nextStatus) {
 321                if (nextStatus == Account.State.OFFLINE
 322                        && account.getStatus() != Account.State.CONNECTING
 323                        && account.getStatus() != Account.State.ONLINE
 324                        && account.getStatus() != Account.State.DISABLED
 325                        && account.getStatus() != Account.State.LOGGED_OUT) {
 326                    return;
 327                }
 328                if (nextStatus == Account.State.ONLINE) {
 329                    this.attempt = 0;
 330                }
 331                account.setStatus(nextStatus);
 332            } else {
 333                return;
 334            }
 335        }
 336        if (statusListener != null) {
 337            statusListener.onStatusChanged(account);
 338        }
 339    }
 340
 341    public Jid getJidForCommand(final String node) {
 342        synchronized (this.commands) {
 343            return this.commands.get(node);
 344        }
 345    }
 346
 347    public void prepareNewConnection() {
 348        this.lastConnectionStarted = SystemClock.elapsedRealtime();
 349        this.lastPingSent = SystemClock.elapsedRealtime();
 350        this.lastDiscoStarted = Long.MAX_VALUE;
 351        this.mWaitingForSmCatchup.set(false);
 352        this.changeStatus(Account.State.CONNECTING);
 353    }
 354
 355    public boolean isWaitingForSmCatchup() {
 356        return mWaitingForSmCatchup.get();
 357    }
 358
 359    public void incrementSmCatchupMessageCounter() {
 360        this.mSmCatchupMessageCounter.incrementAndGet();
 361    }
 362
 363    protected void connect() {
 364        if (mXmppConnectionService.areMessagesInitialized()) {
 365            mXmppConnectionService.resetSendingToWaiting(account);
 366        }
 367        Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": connecting");
 368        this.streamFeatures = null;
 369        this.pendingResumeId.clear();
 370        this.loginInfo = null;
 371        this.features.encryptionEnabled = false;
 372        this.inSmacksSession = false;
 373        this.quickStartInProgress = false;
 374        this.isBound = false;
 375        this.attempt++;
 376        this.dane = false;
 377        this.currentResolverResult = null;
 378        // will be set if user entered hostname is being used or hostname was verified with dnssec
 379        this.verifiedHostname = null;
 380        try {
 381            Socket localSocket;
 382            shouldAuthenticate = !account.isOptionSet(Account.OPTION_REGISTER);
 383            this.changeStatus(Account.State.CONNECTING);
 384            final boolean useTorSetting = appSettings.isUseTor();
 385            final boolean extended = appSettings.isExtendedConnectionOptions();
 386            final boolean useTor = useTorSetting || account.isOnion();
 387            // TODO collapse Tor usage into normal connection code path
 388            if (useTor) {
 389                final var seeOtherHost = this.seeOtherHostResolverResult;
 390                final var hostname = account.getHostname().trim();
 391                final var port = account.getPort();
 392                final Resolver.Result resume = streamId == null ? null : streamId.location;
 393                final Resolver.Result viaTor;
 394                if (resume != null) {
 395                    viaTor = resume;
 396                } else if (seeOtherHost != null) {
 397                    viaTor = seeOtherHost;
 398                } else if (hostname.isEmpty() || port < 0) {
 399                    viaTor =
 400                            Iterables.getOnlyElement(
 401                                    Resolver.fromHardCoded(
 402                                            account.getServer(), Resolver.XMPP_PORT_STARTTLS));
 403                } else {
 404                    if (useTorSetting || extended) {
 405                        // if the hostname configuration is showing we can take it
 406                        viaTor = Iterables.getOnlyElement(Resolver.fromHardCoded(hostname, port));
 407                    } else {
 408                        viaTor =
 409                                Iterables.getOnlyElement(
 410                                        Resolver.fromHardCoded(
 411                                                account.getServer(), Resolver.XMPP_PORT_STARTTLS));
 412                    }
 413                    this.verifiedHostname = hostname;
 414                }
 415
 416                Log.d(Config.LOGTAG, account.getJid().asBareJid() + " via Tor: " + viaTor);
 417
 418                localSocket =
 419                        SocksSocketFactory.createSocketOverTor(
 420                                viaTor.asDestination(), viaTor.getPort());
 421
 422                if (viaTor.isDirectTls()) {
 423                    localSocket = upgradeSocketToTls(localSocket);
 424                    features.encryptionEnabled = true;
 425                }
 426
 427                try {
 428                    if (startXmpp(localSocket)) {
 429                        this.currentResolverResult = viaTor;
 430                        this.seeOtherHostResolverResult = null;
 431                    }
 432                } catch (final InterruptedException e) {
 433                    Log.d(
 434                            Config.LOGTAG,
 435                            account.getJid().asBareJid()
 436                                    + ": thread was interrupted before beginning stream");
 437                    return;
 438                } catch (final Exception e) {
 439                    throw new IOException("Could not start stream", e);
 440                }
 441            } else {
 442                final var hostname = account.getHostname().trim();
 443                final String domain = account.getServer();
 444                final List<Resolver.Result> results = new ArrayList<>();
 445                final boolean hardcoded = extended && !hostname.isEmpty();
 446                if (hardcoded) {
 447                    results.addAll(Resolver.fromHardCoded(hostname, account.getPort()));
 448                } else {
 449                    results.addAll(Resolver.resolve(domain));
 450                }
 451                if (Thread.currentThread().isInterrupted()) {
 452                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Thread was interrupted");
 453                    return;
 454                }
 455                if (results.isEmpty()) {
 456                    Log.e(
 457                            Config.LOGTAG,
 458                            account.getJid().asBareJid() + ": Resolver results were empty");
 459                    return;
 460                }
 461                final Resolver.Result storedBackupResult;
 462                if (hardcoded) {
 463                    storedBackupResult = null;
 464                } else {
 465                    storedBackupResult =
 466                            mXmppConnectionService.databaseBackend.findResolverResult(domain);
 467                    if (storedBackupResult != null && !results.contains(storedBackupResult)) {
 468                        results.add(storedBackupResult);
 469                        Log.d(
 470                                Config.LOGTAG,
 471                                account.getJid().asBareJid()
 472                                        + ": loaded backup resolver result from db: "
 473                                        + storedBackupResult);
 474                    }
 475                }
 476                final StreamId streamId = this.streamId;
 477                final Resolver.Result resumeLocation = streamId == null ? null : streamId.location;
 478                if (resumeLocation != null) {
 479                    Log.d(
 480                            Config.LOGTAG,
 481                            account.getJid().asBareJid()
 482                                    + ": injected resume location on position 0");
 483                    results.add(0, resumeLocation);
 484                }
 485                final Resolver.Result seeOtherHost = this.seeOtherHostResolverResult;
 486                if (seeOtherHost != null) {
 487                    Log.d(
 488                            Config.LOGTAG,
 489                            account.getJid().asBareJid()
 490                                    + ": injected see-other-host on position 0");
 491                    results.add(0, seeOtherHost);
 492                }
 493                for (final Iterator<Resolver.Result> iterator = results.iterator();
 494                        iterator.hasNext(); ) {
 495                    final Resolver.Result result = iterator.next();
 496                    if (Thread.currentThread().isInterrupted()) {
 497                        Log.d(
 498                                Config.LOGTAG,
 499                                account.getJid().asBareJid() + ": Thread was interrupted");
 500                        return;
 501                    }
 502                    try {
 503                        // if tls is true, encryption is implied and must not be started
 504                        features.encryptionEnabled = result.isDirectTls();
 505                        verifiedHostname =
 506                                result.isAuthenticated() ? result.getHostname().toString() : null;
 507                        final InetSocketAddress addr;
 508                        if (result.getIp() != null) {
 509                            addr = new InetSocketAddress(result.getIp(), result.getPort());
 510                            Log.d(
 511                                    Config.LOGTAG,
 512                                    account.getJid().asBareJid().toString()
 513                                            + ": using values from resolver "
 514                                            + (result.getHostname() == null
 515                                                    ? ""
 516                                                    : result.getHostname().toString() + "/")
 517                                            + result.getIp().getHostAddress()
 518                                            + ":"
 519                                            + result.getPort()
 520                                            + " tls: "
 521                                            + features.encryptionEnabled);
 522                        } else {
 523                            addr =
 524                                    new InetSocketAddress(
 525                                            IDN.toASCII(result.getHostname().toString()),
 526                                            result.getPort());
 527                            Log.d(
 528                                    Config.LOGTAG,
 529                                    account.getJid().asBareJid().toString()
 530                                            + ": using values from resolver "
 531                                            + result.getHostname().toString()
 532                                            + ":"
 533                                            + result.getPort()
 534                                            + " tls: "
 535                                            + features.encryptionEnabled);
 536                        }
 537
 538                        localSocket = new Socket();
 539                        localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
 540                        localSocket.setSoTimeout(Config.SOCKET_TIMEOUT * 1000);
 541                        if (features.encryptionEnabled) {
 542                            localSocket = upgradeSocketToTls(localSocket);
 543                        }
 544                        if (startXmpp(localSocket)) {
 545                            // reset to 0; once the connection is established we don't want this
 546                            localSocket.setSoTimeout(0);
 547                            if (!hardcoded && !result.equals(storedBackupResult)) {
 548                                mXmppConnectionService.databaseBackend.saveResolverResult(
 549                                        domain, result);
 550                            }
 551                            this.currentResolverResult = result;
 552                            this.seeOtherHostResolverResult = null;
 553                            break; // successfully connected to server that speaks xmpp
 554                        } else {
 555                            FileBackend.close(localSocket);
 556                            throw new StateChangingException(Account.State.STREAM_OPENING_ERROR);
 557                        }
 558                    } catch (final StateChangingException e) {
 559                        if (!iterator.hasNext()) {
 560                            throw e;
 561                        }
 562                    } catch (InterruptedException e) {
 563                        Log.d(
 564                                Config.LOGTAG,
 565                                account.getJid().asBareJid()
 566                                        + ": thread was interrupted before beginning stream");
 567                        return;
 568                    } catch (final Throwable e) {
 569                        Log.d(
 570                                Config.LOGTAG,
 571                                account.getJid().asBareJid().toString()
 572                                        + ": "
 573                                        + e.getMessage()
 574                                        + "("
 575                                        + e.getClass().getName()
 576                                        + ")");
 577                        if (!iterator.hasNext()) {
 578                            throw new UnknownHostException();
 579                        }
 580                    }
 581                }
 582            }
 583            processStream();
 584        } catch (final SecurityException e) {
 585            this.changeStatus(Account.State.MISSING_INTERNET_PERMISSION);
 586        } catch (final StateChangingException e) {
 587            this.changeStatus(e.state);
 588        } catch (final UnknownHostException
 589                | ConnectException
 590                | SocksSocketFactory.HostNotFoundException e) {
 591            this.changeStatus(Account.State.SERVER_NOT_FOUND);
 592        } catch (final SocksSocketFactory.SocksProxyNotFoundException e) {
 593            this.changeStatus(Account.State.TOR_NOT_AVAILABLE);
 594        } catch (final IOException | XmlPullParserException e) {
 595            Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": " + e.getMessage());
 596            this.changeStatus(Account.State.OFFLINE);
 597            this.attempt = Math.max(0, this.attempt - 1);
 598        } finally {
 599            if (!Thread.currentThread().isInterrupted()) {
 600                forceCloseSocket();
 601            } else {
 602                Log.d(
 603                        Config.LOGTAG,
 604                        account.getJid().asBareJid()
 605                                + ": not force closing socket because thread was interrupted");
 606            }
 607        }
 608    }
 609
 610    /**
 611     * Starts xmpp protocol, call after connecting to socket
 612     *
 613     * @return true if server returns with valid xmpp, false otherwise
 614     */
 615    private boolean startXmpp(final Socket socket) throws Exception {
 616        if (Thread.currentThread().isInterrupted()) {
 617            throw new InterruptedException();
 618        }
 619        // this means we have at least found a socket to connect to. give the connection another 90s
 620        this.lastConnectionStarted = SystemClock.elapsedRealtime();
 621        this.socket = socket;
 622        this.tagReader = new XmlReader();
 623        if (tagWriter != null) {
 624            tagWriter.forceClose();
 625        }
 626        this.tagWriter = new TagWriter();
 627        this.tagWriter.setOutputStream(socket.getOutputStream());
 628        this.tagReader.setInputStream(socket.getInputStream());
 629        this.tagWriter.beginDocument();
 630        final boolean quickStart;
 631        if (socket instanceof SSLSocket sslSocket) {
 632            SSLSockets.log(account, sslSocket);
 633            quickStart = establishStream(SSLSockets.version(sslSocket));
 634        } else {
 635            quickStart = establishStream(SSLSockets.Version.NONE);
 636        }
 637        final Tag tag = tagReader.readTag();
 638        if (Thread.currentThread().isInterrupted()) {
 639            throw new InterruptedException();
 640        }
 641        if (tag == null) {
 642            return false;
 643        }
 644        final boolean success = tag.isStart("stream", Namespace.STREAMS);
 645        if (success) {
 646            final var from = tag.getAttribute("from");
 647            if (from == null || !from.equals(account.getServer())) {
 648                throw new StateChangingException(Account.State.HOST_UNKNOWN);
 649            }
 650        }
 651        if (success && quickStart) {
 652            this.quickStartInProgress = true;
 653        }
 654        return success;
 655    }
 656
 657    private SSLSocketFactory getSSLSocketFactory(int port, Consumer<Boolean> daneCb)
 658            throws NoSuchAlgorithmException, KeyManagementException {
 659        final SSLContext sc = SSLSockets.getSSLContext();
 660        final MemorizingTrustManager trustManager =
 661                this.mXmppConnectionService.getMemorizingTrustManager();
 662        final KeyManager[] keyManager;
 663        if (account.getPrivateKeyAlias() != null) {
 664            keyManager = new KeyManager[] {new MyKeyManager()};
 665        } else {
 666            keyManager = null;
 667        }
 668        final String domain = account.getServer();
 669        sc.init(
 670                keyManager,
 671                new X509TrustManager[] {
 672                    mInteractive
 673                            ? trustManager.getInteractive(domain, verifiedHostname, port, daneCb)
 674                            : trustManager.getNonInteractive(domain, verifiedHostname, port, daneCb)
 675                },
 676                SECURE_RANDOM);
 677        return sc.getSocketFactory();
 678    }
 679
 680    @Override
 681    public void run() {
 682        synchronized (this) {
 683            this.mThread = Thread.currentThread();
 684            if (this.mThread.isInterrupted()) {
 685                Log.d(
 686                        Config.LOGTAG,
 687                        account.getJid().asBareJid()
 688                                + ": aborting connect because thread was interrupted");
 689                return;
 690            }
 691            forceCloseSocket();
 692        }
 693        connect();
 694    }
 695
 696    private void processStream() throws XmlPullParserException, IOException {
 697        final CountDownLatch streamCountDownLatch = new CountDownLatch(1);
 698        this.mStreamCountDownLatch = streamCountDownLatch;
 699        Tag nextTag = tagReader.readTag();
 700        while (nextTag != null && !nextTag.isEnd("stream")) {
 701            if (nextTag.isStart("error", Namespace.STREAMS)) {
 702                processStreamError(tagReader.readElement(nextTag, StreamError.class));
 703            } else if (nextTag.isStart("features", Namespace.STREAMS)) {
 704                processStreamFeatures(nextTag);
 705            } else if (nextTag.isStart("proceed", Namespace.TLS)) {
 706                if (this.socket instanceof SSLSocket) {
 707                    throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 708                }
 709                switchOverToTls(nextTag);
 710            } else if (nextTag.isStart("failure", Namespace.TLS)) {
 711                throw new StateChangingException(Account.State.TLS_ERROR);
 712            } else if (!isSecure()) {
 713                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 714            } else if (account.isOptionSet(Account.OPTION_REGISTER)
 715                    && nextTag.isStart("iq", Namespace.JABBER_CLIENT)) {
 716                processIq(nextTag);
 717            } else if (this.loginInfo == null) {
 718                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 719            } else if (nextTag.isStart("success", Namespace.SASL)) {
 720                processSuccess(tagReader.readElement(nextTag, Success.class));
 721                break;
 722            } else if (nextTag.isStart("success", Namespace.SASL_2)) {
 723                processSuccess(
 724                        tagReader.readElement(
 725                                nextTag, im.conversations.android.xmpp.model.sasl2.Success.class));
 726            } else if (nextTag.isStart("failure", Namespace.SASL)) {
 727                final var failure = tagReader.readElement(nextTag, Failure.class);
 728                processFailure(failure);
 729            } else if (nextTag.isStart("failure", Namespace.SASL_2)) {
 730                final var failure =
 731                        tagReader.readElement(
 732                                nextTag, im.conversations.android.xmpp.model.sasl2.Failure.class);
 733                processFailure(failure);
 734            } else if (nextTag.isStart("continue", Namespace.SASL_2)) {
 735                // two step sasl2 - we don’t support this yet
 736                throw new StateChangingException(Account.State.INCOMPATIBLE_CLIENT);
 737            } else if (nextTag.isStart("challenge")) {
 738                final Element challenge = tagReader.readElement(nextTag);
 739                processChallenge(challenge);
 740            } else if (!LoginInfo.isSuccess(this.loginInfo)) {
 741                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 742            } else if (this.streamId != null
 743                    && nextTag.isStart("resumed", Namespace.STREAM_MANAGEMENT)) {
 744                final Resumed resumed = tagReader.readElement(nextTag, Resumed.class);
 745                processResumed(resumed);
 746            } else if (nextTag.isStart("failed", Namespace.STREAM_MANAGEMENT)) {
 747                final Failed failed = tagReader.readElement(nextTag, Failed.class);
 748                processFailed(failed, true);
 749            } else if (nextTag.isStart("iq", Namespace.JABBER_CLIENT)) {
 750                processIq(nextTag);
 751            } else if (!isBound) {
 752                Log.d(
 753                        Config.LOGTAG,
 754                        account.getJid().asBareJid()
 755                                + ": server sent unexpected"
 756                                + nextTag.identifier());
 757                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 758            } else if (nextTag.isStart("message", Namespace.JABBER_CLIENT)) {
 759                processMessage(nextTag);
 760            } else if (nextTag.isStart("presence", Namespace.JABBER_CLIENT)) {
 761                processPresence(nextTag);
 762            } else if (nextTag.isStart("enabled", Namespace.STREAM_MANAGEMENT)) {
 763                final var enabled = tagReader.readElement(nextTag, Enabled.class);
 764                processEnabled(enabled);
 765            } else if (nextTag.isStart("r", Namespace.STREAM_MANAGEMENT)) {
 766                tagReader.readElement(nextTag);
 767                if (Config.EXTENDED_SM_LOGGING) {
 768                    Log.d(
 769                            Config.LOGTAG,
 770                            account.getJid().asBareJid()
 771                                    + ": acknowledging stanza #"
 772                                    + this.stanzasReceived);
 773                }
 774                final Ack ack = new Ack(this.stanzasReceived);
 775                tagWriter.writeStanzaAsync(ack);
 776            } else if (nextTag.isStart("a", Namespace.STREAM_MANAGEMENT)) {
 777                boolean accountUiNeedsRefresh = false;
 778                synchronized (NotificationService.CATCHUP_LOCK) {
 779                    if (mWaitingForSmCatchup.compareAndSet(true, false)) {
 780                        final int messageCount = mSmCatchupMessageCounter.get();
 781                        final int pendingIQs = packetCallbacks.size();
 782                        Log.d(
 783                                Config.LOGTAG,
 784                                account.getJid().asBareJid()
 785                                        + ": SM catchup complete (messages="
 786                                        + messageCount
 787                                        + ", pending IQs="
 788                                        + pendingIQs
 789                                        + ")");
 790                        accountUiNeedsRefresh = true;
 791                        if (messageCount > 0) {
 792                            mXmppConnectionService
 793                                    .getNotificationService()
 794                                    .finishBacklog(true, account);
 795                        }
 796                    }
 797                }
 798                if (accountUiNeedsRefresh) {
 799                    mXmppConnectionService.updateAccountUi();
 800                }
 801                final var ack = tagReader.readElement(nextTag, Ack.class);
 802                lastPacketReceived = SystemClock.elapsedRealtime();
 803                final boolean acknowledgedMessages;
 804                synchronized (this.mStanzaQueue) {
 805                    final Optional<Integer> serverSequence = ack.getHandled();
 806                    if (serverSequence.isPresent()) {
 807                        acknowledgedMessages = acknowledgeStanzaUpTo(serverSequence.get());
 808                    } else {
 809                        acknowledgedMessages = false;
 810                        Log.d(
 811                                Config.LOGTAG,
 812                                account.getJid().asBareJid()
 813                                        + ": server send ack without sequence number");
 814                    }
 815                }
 816                if (acknowledgedMessages) {
 817                    mXmppConnectionService.updateConversationUi();
 818                }
 819            } else {
 820                Log.e(
 821                        Config.LOGTAG,
 822                        account.getJid().asBareJid()
 823                                + ": Encountered unknown stream element"
 824                                + nextTag.identifier());
 825                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 826            }
 827            nextTag = tagReader.readTag();
 828        }
 829        if (nextTag != null && nextTag.isEnd("stream")) {
 830            streamCountDownLatch.countDown();
 831        }
 832    }
 833
 834    private void processChallenge(final Element challenge) throws IOException {
 835        final SaslMechanism.Version version;
 836        try {
 837            version = SaslMechanism.Version.of(challenge);
 838        } catch (final IllegalArgumentException e) {
 839            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 840        }
 841        final StreamElement response;
 842        if (version == SaslMechanism.Version.SASL) {
 843            response = new Response();
 844        } else if (version == SaslMechanism.Version.SASL_2) {
 845            response = new im.conversations.android.xmpp.model.sasl2.Response();
 846        } else {
 847            throw new AssertionError("Missing implementation for " + version);
 848        }
 849        final LoginInfo currentLoginInfo = this.loginInfo;
 850        if (currentLoginInfo == null || LoginInfo.isSuccess(currentLoginInfo)) {
 851            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 852        }
 853        try {
 854            response.setContent(
 855                    currentLoginInfo.saslMechanism.getResponse(
 856                            challenge.getContent(), sslSocketOrNull(socket)));
 857        } catch (final SaslMechanism.AuthenticationException e) {
 858            // TODO: Send auth abort tag.
 859            Log.e(Config.LOGTAG, e.toString());
 860            throw new StateChangingException(Account.State.UNAUTHORIZED);
 861        }
 862        tagWriter.writeElement(response);
 863    }
 864
 865    private void processSuccess(final StreamElement element)
 866            throws IOException, XmlPullParserException {
 867        final LoginInfo currentLoginInfo = this.loginInfo;
 868        final SaslMechanism currentSaslMechanism = LoginInfo.mechanism(currentLoginInfo);
 869        if (currentLoginInfo == null
 870                || LoginInfo.isSuccess(currentLoginInfo)
 871                || currentSaslMechanism == null) {
 872            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 873        }
 874        final SaslMechanism.Version version;
 875        final String challenge;
 876        if (element instanceof Success success) {
 877            challenge = success.getContent();
 878            version = SaslMechanism.Version.SASL;
 879        } else if (element instanceof im.conversations.android.xmpp.model.sasl2.Success success) {
 880            challenge = success.findChildContent("additional-data");
 881            version = SaslMechanism.Version.SASL_2;
 882        } else {
 883            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 884        }
 885        try {
 886            currentLoginInfo.success(challenge, sslSocketOrNull(socket));
 887        } catch (final SaslMechanism.AuthenticationException e) {
 888            Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": authentication failure ", e);
 889            throw new StateChangingException(Account.State.UNAUTHORIZED);
 890        }
 891        Log.d(
 892                Config.LOGTAG,
 893                account.getJid().asBareJid().toString() + ": logged in (using " + version + ")");
 894        if (SaslMechanism.pin(currentSaslMechanism)) {
 895            account.setPinnedMechanism(currentSaslMechanism);
 896        }
 897        if (element instanceof im.conversations.android.xmpp.model.sasl2.Success success) {
 898            final var authorizationJid = success.getAuthorizationIdentifier();
 899            checkAssignedDomainOrThrow(authorizationJid);
 900            Log.d(
 901                    Config.LOGTAG,
 902                    account.getJid().asBareJid()
 903                            + ": SASL 2.0 authorization identifier was "
 904                            + authorizationJid);
 905            // TODO this should only happen when we used Bind 2
 906            if (authorizationJid.isFullJid() && account.setJid(authorizationJid)) {
 907                Log.d(
 908                        Config.LOGTAG,
 909                        account.getJid().asBareJid()
 910                                + ": jid changed during SASL 2.0. updating database");
 911            }
 912            final Bound bound = success.getExtension(Bound.class);
 913            final Resumed resumed = success.getExtension(Resumed.class);
 914            final Failed failed = success.getExtension(Failed.class);
 915            final Element tokenWrapper = success.findChild("token", Namespace.FAST);
 916            final String token = tokenWrapper == null ? null : tokenWrapper.getAttribute("token");
 917            if (bound != null && resumed != null) {
 918                Log.d(
 919                        Config.LOGTAG,
 920                        account.getJid().asBareJid()
 921                                + ": server sent bound and resumed in SASL2 success");
 922                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 923            }
 924            if (resumed != null && streamId != null) {
 925                if (this.boundStreamFeatures != null) {
 926                    this.streamFeatures = this.boundStreamFeatures;
 927                    Log.d(
 928                            Config.LOGTAG,
 929                            "putting previous stream features back in place: "
 930                                    + XmlHelper.printElementNames(this.boundStreamFeatures));
 931                }
 932                processResumed(resumed);
 933            } else if (failed != null) {
 934                processFailed(failed, false); // wait for new stream features
 935            }
 936            if (bound != null) {
 937                clearIqCallbacks();
 938                this.isBound = true;
 939                processNopStreamFeatures();
 940                this.boundStreamFeatures = this.streamFeatures;
 941                final Enabled streamManagementEnabled = bound.getExtension(Enabled.class);
 942                final Element carbonsEnabled = bound.findChild("enabled", Namespace.CARBONS);
 943                final boolean waitForDisco;
 944                if (streamManagementEnabled != null) {
 945                    resetOutboundStanzaQueue();
 946                    processEnabled(streamManagementEnabled);
 947                    waitForDisco = true;
 948                } else {
 949                    // if we did not enable stream management in bind do it now
 950                    waitForDisco = enableStreamManagement();
 951                }
 952                final boolean negotiatedCarbons;
 953                if (carbonsEnabled != null) {
 954                    negotiatedCarbons = true;
 955                    Log.d(
 956                            Config.LOGTAG,
 957                            account.getJid().asBareJid()
 958                                    + ": successfully enabled carbons (via Bind 2.0)");
 959                    features.carbonsEnabled = true;
 960                } else if (currentLoginInfo.inlineBindFeatures != null
 961                        && currentLoginInfo.inlineBindFeatures.contains(Namespace.CARBONS)) {
 962                    negotiatedCarbons = true;
 963                    Log.d(
 964                            Config.LOGTAG,
 965                            account.getJid().asBareJid()
 966                                    + ": successfully enabled carbons (via Bind 2.0/implicit)");
 967                    features.carbonsEnabled = true;
 968                } else {
 969                    negotiatedCarbons = false;
 970                }
 971                sendPostBindInitialization(waitForDisco, negotiatedCarbons);
 972            }
 973            final HashedToken.Mechanism tokenMechanism;
 974            if (SaslMechanism.hashedToken(currentSaslMechanism)) {
 975                tokenMechanism = ((HashedToken) currentSaslMechanism).getTokenMechanism();
 976            } else if (this.hashTokenRequest != null) {
 977                tokenMechanism = this.hashTokenRequest;
 978            } else {
 979                tokenMechanism = null;
 980            }
 981            if (tokenMechanism != null && !Strings.isNullOrEmpty(token)) {
 982                if (ChannelBinding.priority(tokenMechanism.channelBinding)
 983                        >= ChannelBindingMechanism.getPriority(currentSaslMechanism)) {
 984                    this.account.setFastToken(tokenMechanism, token);
 985                    Log.d(
 986                            Config.LOGTAG,
 987                            account.getJid().asBareJid()
 988                                    + ": storing hashed token "
 989                                    + tokenMechanism);
 990                } else {
 991                    Log.d(
 992                            Config.LOGTAG,
 993                            account.getJid().asBareJid()
 994                                    + ": not accepting hashed token "
 995                                    + tokenMechanism.name()
 996                                    + " for log in mechanism "
 997                                    + currentSaslMechanism.getMechanism());
 998                    this.account.resetFastToken();
 999                }
1000            } else if (this.hashTokenRequest != null) {
1001                Log.w(
1002                        Config.LOGTAG,
1003                        account.getJid().asBareJid()
1004                                + ": no response to our hashed token request "
1005                                + this.hashTokenRequest);
1006            }
1007        }
1008        mXmppConnectionService.databaseBackend.updateAccount(account);
1009        this.quickStartInProgress = false;
1010        if (version == SaslMechanism.Version.SASL) {
1011            tagReader.reset();
1012            sendStartStream(false, true);
1013            final Tag tag = tagReader.readTag();
1014            if (tag != null && tag.isStart("stream", Namespace.STREAMS)) {
1015                processStream();
1016            } else {
1017                throw new StateChangingException(Account.State.STREAM_OPENING_ERROR);
1018            }
1019        }
1020    }
1021
1022    private void resetOutboundStanzaQueue() {
1023        synchronized (this.mStanzaQueue) {
1024            final ImmutableList.Builder<Stanza> intermediateStanzasBuilder =
1025                    new ImmutableList.Builder<>();
1026            if (Config.EXTENDED_SM_LOGGING) {
1027                Log.d(
1028                        Config.LOGTAG,
1029                        account.getJid().asBareJid()
1030                                + ": stanzas sent before auth: "
1031                                + this.stanzasSentBeforeAuthentication);
1032            }
1033            for (int i = this.stanzasSentBeforeAuthentication + 1; i <= this.stanzasSent; ++i) {
1034                final Stanza stanza = this.mStanzaQueue.get(i);
1035                if (stanza != null) {
1036                    intermediateStanzasBuilder.add(stanza);
1037                }
1038            }
1039            this.mStanzaQueue.clear();
1040            final var intermediateStanzas = intermediateStanzasBuilder.build();
1041            for (int i = 0; i < intermediateStanzas.size(); ++i) {
1042                this.mStanzaQueue.append(i + 1, intermediateStanzas.get(i));
1043            }
1044            this.stanzasSent = intermediateStanzas.size();
1045            if (Config.EXTENDED_SM_LOGGING) {
1046                Log.d(
1047                        Config.LOGTAG,
1048                        account.getJid().asBareJid()
1049                                + ": resetting outbound stanza queue to "
1050                                + this.stanzasSent);
1051            }
1052        }
1053    }
1054
1055    private void processNopStreamFeatures() throws IOException {
1056        final Tag tag = tagReader.readTag();
1057        if (tag != null && tag.isStart("features", Namespace.STREAMS)) {
1058            this.streamFeatures =
1059                    tagReader.readElement(
1060                            tag, im.conversations.android.xmpp.model.streams.Features.class);
1061            Log.d(
1062                    Config.LOGTAG,
1063                    account.getJid().asBareJid()
1064                            + ": processed NOP stream features after success: "
1065                            + XmlHelper.printElementNames(this.streamFeatures));
1066        } else {
1067            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received " + tag);
1068            Log.d(
1069                    Config.LOGTAG,
1070                    account.getJid().asBareJid()
1071                            + ": server did not send stream features after SASL2 success");
1072            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1073        }
1074    }
1075
1076    private void processFailure(final AuthenticationFailure failure) throws IOException {
1077        final SaslMechanism.Version version;
1078        try {
1079            version = SaslMechanism.Version.of(failure);
1080        } catch (final IllegalArgumentException e) {
1081            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1082        }
1083
1084        final LoginInfo currentLoginInfo = this.loginInfo;
1085        if (currentLoginInfo == null || LoginInfo.isSuccess(currentLoginInfo)) {
1086            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1087        }
1088
1089        Log.d(Config.LOGTAG, failure.toString());
1090        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version);
1091        if (SaslMechanism.hashedToken(LoginInfo.mechanism(currentLoginInfo))) {
1092            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resetting token");
1093            account.resetFastToken();
1094            mXmppConnectionService.databaseBackend.updateAccount(account);
1095        }
1096        final var errorCondition = failure.getErrorCondition();
1097        if (errorCondition instanceof SaslError.InvalidMechanism
1098                || errorCondition instanceof SaslError.MechanismTooWeak) {
1099            Log.d(
1100                    Config.LOGTAG,
1101                    account.getJid().asBareJid()
1102                            + ": invalid or too weak mechanism. resetting quick start");
1103            if (account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, false)) {
1104                mXmppConnectionService.databaseBackend.updateAccount(account);
1105            }
1106            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1107        } else if (errorCondition instanceof SaslError.TemporaryAuthFailure) {
1108            throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE);
1109        } else if (errorCondition instanceof SaslError.AccountDisabled) {
1110            final String text = failure.getText();
1111            if (Strings.isNullOrEmpty(text)) {
1112                throw new StateChangingException(Account.State.UNAUTHORIZED);
1113            }
1114            final Matcher matcher = Patterns.URI_HTTP.matcher(text);
1115            if (matcher.find()) {
1116                final HttpUrl url;
1117                try {
1118                    url = HttpUrl.get(text.substring(matcher.start(), matcher.end()));
1119                } catch (final IllegalArgumentException e) {
1120                    throw new StateChangingException(Account.State.UNAUTHORIZED);
1121                }
1122                if (url.isHttps()) {
1123                    this.redirectionUrl = url;
1124                    throw new StateChangingException(Account.State.PAYMENT_REQUIRED);
1125                }
1126            }
1127        }
1128        if (SaslMechanism.hashedToken(LoginInfo.mechanism(currentLoginInfo))) {
1129            Log.d(
1130                    Config.LOGTAG,
1131                    account.getJid().asBareJid()
1132                            + ": fast authentication failed. falling back to regular"
1133                            + " authentication");
1134            this.loginInfo = null;
1135            authenticate();
1136        } else {
1137            throw new StateChangingException(Account.State.UNAUTHORIZED);
1138        }
1139    }
1140
1141    private static SSLSocket sslSocketOrNull(final Socket socket) {
1142        if (socket instanceof SSLSocket) {
1143            return (SSLSocket) socket;
1144        } else {
1145            return null;
1146        }
1147    }
1148
1149    private void processEnabled(final Enabled enabled) {
1150        final StreamId streamId = getStreamId(enabled);
1151        if (streamId == null) {
1152            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream management enabled");
1153        } else {
1154            Log.d(
1155                    Config.LOGTAG,
1156                    account.getJid().asBareJid()
1157                            + ": stream management enabled. resume at: "
1158                            + streamId.location);
1159        }
1160        this.streamId = streamId;
1161        this.stanzasReceived = 0;
1162        this.inSmacksSession = true;
1163        final var r = new Request();
1164        tagWriter.writeStanzaAsync(r);
1165    }
1166
1167    @Nullable
1168    private StreamId getStreamId(final Enabled enabled) {
1169        final Optional<String> id = enabled.getResumeId();
1170        final String locationAttribute = enabled.getLocation();
1171        final Resolver.Result currentResolverResult = this.currentResolverResult;
1172        final Resolver.Result location;
1173        if (Strings.isNullOrEmpty(locationAttribute) || currentResolverResult == null) {
1174            location = null;
1175        } else {
1176            location = currentResolverResult.seeOtherHost(locationAttribute);
1177        }
1178        return id.isPresent() ? new StreamId(id.get(), location) : null;
1179    }
1180
1181    private void processResumed(final Resumed resumed) throws StateChangingException {
1182        final var pendingResumeId = this.pendingResumeId.pop();
1183        final var prevId = resumed.getPrevId();
1184        if (prevId == null || !prevId.equals(pendingResumeId)) {
1185            Log.d(
1186                    Config.LOGTAG,
1187                    account.getJid().asBareJid()
1188                            + ": server tried resume with unknown id "
1189                            + prevId);
1190            resetStreamId();
1191            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1192        }
1193        this.inSmacksSession = true;
1194        this.isBound = true;
1195        this.tagWriter.writeStanzaAsync(new Request());
1196        lastPacketReceived = SystemClock.elapsedRealtime();
1197        final Optional<Integer> h = resumed.getHandled();
1198        final int serverCount;
1199        if (h.isPresent()) {
1200            serverCount = h.get();
1201        } else {
1202            resetStreamId();
1203            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1204        }
1205        final ArrayList<Stanza> failedStanzas = new ArrayList<>();
1206        final boolean acknowledgedMessages;
1207        synchronized (this.mStanzaQueue) {
1208            if (serverCount < stanzasSent) {
1209                Log.d(
1210                        Config.LOGTAG,
1211                        account.getJid().asBareJid() + ": session resumed with lost packages");
1212                stanzasSent = serverCount;
1213            } else {
1214                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": session resumed");
1215            }
1216            acknowledgedMessages = acknowledgeStanzaUpTo(serverCount);
1217            for (int i = 0; i < this.mStanzaQueue.size(); ++i) {
1218                failedStanzas.add(mStanzaQueue.valueAt(i));
1219            }
1220            mStanzaQueue.clear();
1221        }
1222        if (acknowledgedMessages) {
1223            mXmppConnectionService.updateConversationUi();
1224        }
1225        Log.d(
1226                Config.LOGTAG,
1227                account.getJid().asBareJid() + ": resending " + failedStanzas.size() + " stanzas");
1228        for (final Stanza packet : failedStanzas) {
1229            if (packet instanceof im.conversations.android.xmpp.model.stanza.Message message) {
1230                mXmppConnectionService.markMessage(
1231                        account,
1232                        message.getTo().asBareJid(),
1233                        message.getId(),
1234                        Message.STATUS_UNSEND);
1235            }
1236            sendPacket(packet);
1237        }
1238        if (mWaitForDisco.get()) {
1239            this.lastDiscoStarted = SystemClock.elapsedRealtime();
1240            Log.d(
1241                    Config.LOGTAG,
1242                    account.getJid().asBareJid() + ": awaiting disco results after resume");
1243            changeStatus(Account.State.CONNECTING);
1244        } else {
1245            changeStatusToOnline();
1246        }
1247    }
1248
1249    private void changeStatusToOnline() {
1250        Log.d(
1251                Config.LOGTAG,
1252                account.getJid().asBareJid() + ": online with resource " + account.getResource());
1253        changeStatus(Account.State.ONLINE);
1254    }
1255
1256    private void processFailed(final Failed failed, final boolean sendBindRequest) {
1257        final Optional<Integer> serverCount = failed.getHandled();
1258        if (serverCount.isPresent()) {
1259            Log.d(
1260                    Config.LOGTAG,
1261                    account.getJid().asBareJid()
1262                            + ": resumption failed but server acknowledged stanza #"
1263                            + serverCount.get());
1264            final boolean acknowledgedMessages;
1265            synchronized (this.mStanzaQueue) {
1266                acknowledgedMessages = acknowledgeStanzaUpTo(serverCount.get());
1267            }
1268            if (acknowledgedMessages) {
1269                mXmppConnectionService.updateConversationUi();
1270            }
1271        } else {
1272            Log.d(
1273                    Config.LOGTAG,
1274                    account.getJid().asBareJid()
1275                            + ": resumption failed ("
1276                            + XmlHelper.print(failed.getChildren())
1277                            + ")");
1278        }
1279        resetStreamId();
1280        if (sendBindRequest) {
1281            sendBindRequest();
1282        }
1283    }
1284
1285    private boolean acknowledgeStanzaUpTo(final int serverCount) {
1286        if (serverCount > stanzasSent) {
1287            Log.e(
1288                    Config.LOGTAG,
1289                    "server acknowledged more stanzas than we sent. serverCount="
1290                            + serverCount
1291                            + ", ourCount="
1292                            + stanzasSent);
1293        }
1294        boolean acknowledgedMessages = false;
1295        for (int i = 0; i < mStanzaQueue.size(); ++i) {
1296            if (serverCount >= mStanzaQueue.keyAt(i)) {
1297                if (Config.EXTENDED_SM_LOGGING) {
1298                    Log.d(
1299                            Config.LOGTAG,
1300                            account.getJid().asBareJid()
1301                                    + ": server acknowledged stanza #"
1302                                    + mStanzaQueue.keyAt(i));
1303                }
1304                final Stanza stanza = mStanzaQueue.valueAt(i);
1305                if (stanza instanceof im.conversations.android.xmpp.model.stanza.Message packet
1306                        && acknowledgedListener != null) {
1307                    final String id = packet.getId();
1308                    final Jid to = packet.getTo();
1309                    if (id != null && to != null) {
1310                        acknowledgedMessages |=
1311                                acknowledgedListener.onMessageAcknowledged(account, to, id);
1312                    }
1313                }
1314                mStanzaQueue.removeAt(i);
1315                i--;
1316            }
1317        }
1318        return acknowledgedMessages;
1319    }
1320
1321    private <S extends Stanza> @NonNull S processPacket(final Tag currentTag, final Class<S> clazz)
1322            throws IOException {
1323        final S stanza = tagReader.readElement(currentTag, clazz);
1324        if (stanzasReceived == Integer.MAX_VALUE) {
1325            resetStreamId();
1326            throw new IOException("time to restart the session. cant handle >2 billion pcks");
1327        }
1328        if (inSmacksSession) {
1329            ++stanzasReceived;
1330        } else if (features.sm()) {
1331            Log.d(
1332                    Config.LOGTAG,
1333                    account.getJid().asBareJid()
1334                            + ": not counting stanza("
1335                            + stanza.getClass().getSimpleName()
1336                            + "). Not in smacks session.");
1337        }
1338        lastPacketReceived = SystemClock.elapsedRealtime();
1339        if (Config.BACKGROUND_STANZA_LOGGING && mXmppConnectionService.checkListeners()) {
1340            Log.d(Config.LOGTAG, "[background stanza] " + stanza);
1341        }
1342        return stanza;
1343    }
1344
1345    private void processIq(final Tag currentTag) throws IOException {
1346        final Iq packet = processPacket(currentTag, Iq.class);
1347        if (packet.isInvalid()) {
1348            Log.e(
1349                    Config.LOGTAG,
1350                    "encountered invalid iq from='"
1351                            + packet.getFrom()
1352                            + "' to='"
1353                            + packet.getTo()
1354                            + "'");
1355            return;
1356        }
1357        if (Thread.currentThread().isInterrupted()) {
1358            Log.d(
1359                    Config.LOGTAG,
1360                    account.getJid().asBareJid() + "Not processing iq. Thread was interrupted");
1361            return;
1362        }
1363        if (packet.hasExtension(Jingle.class)
1364                && packet.getType() == Iq.Type.SET
1365                && isBound
1366                && LoginInfo.isSuccess(this.loginInfo)) {
1367            if (this.jingleListener != null) {
1368                this.jingleListener.onJinglePacketReceived(account, packet);
1369            }
1370        } else {
1371            final var callback = getIqPacketReceivedCallback(packet);
1372            if (callback == null) {
1373                Log.d(
1374                        Config.LOGTAG,
1375                        account.getJid().asBareJid().toString()
1376                                + ": no callback registered for IQ from "
1377                                + packet.getFrom());
1378                return;
1379            }
1380            final ScheduledFuture timeoutFuture = callback.second;
1381            try {
1382                if (timeoutFuture == null || timeoutFuture.cancel(false)) {
1383                    callback.first.accept(packet);
1384                }
1385            } catch (final StateChangingError error) {
1386                throw new StateChangingException(error.state);
1387            }
1388        }
1389    }
1390
1391    private Pair<Consumer<Iq>, ScheduledFuture> getIqPacketReceivedCallback(final Iq stanza)
1392            throws StateChangingException {
1393        final boolean isRequest =
1394                stanza.getType() == Iq.Type.GET || stanza.getType() == Iq.Type.SET;
1395        if (isRequest) {
1396            if (isBound && LoginInfo.isSuccess(this.loginInfo)) {
1397                return new Pair<>(this.unregisteredIqListener, null);
1398            } else {
1399                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1400            }
1401        } else {
1402            synchronized (this.packetCallbacks) {
1403                final var pair = packetCallbacks.get(stanza.getId());
1404                if (pair == null) {
1405                    return null;
1406                }
1407                if (pair.first.toServer(account)) {
1408                    if (stanza.fromServer(account)) {
1409                        packetCallbacks.remove(stanza.getId());
1410                        return pair.second;
1411                    } else {
1412                        Log.e(
1413                                Config.LOGTAG,
1414                                account.getJid().asBareJid().toString()
1415                                        + ": ignoring spoofed iq packet");
1416                    }
1417                } else {
1418                    if (stanza.getFrom() != null && stanza.getFrom().equals(pair.first.getTo())) {
1419                        packetCallbacks.remove(stanza.getId());
1420                        return pair.second;
1421                    } else {
1422                        Log.e(
1423                                Config.LOGTAG,
1424                                account.getJid().asBareJid().toString()
1425                                        + ": ignoring spoofed iq packet");
1426                    }
1427                }
1428            }
1429        }
1430        return null;
1431    }
1432
1433    private void processMessage(final Tag currentTag) throws IOException {
1434        final var packet =
1435                processPacket(currentTag, im.conversations.android.xmpp.model.stanza.Message.class);
1436        if (packet.isInvalid()) {
1437            Log.e(
1438                    Config.LOGTAG,
1439                    "encountered invalid message from='"
1440                            + packet.getFrom()
1441                            + "' to='"
1442                            + packet.getTo()
1443                            + "'");
1444            return;
1445        }
1446        if (Thread.currentThread().isInterrupted()) {
1447            Log.d(
1448                    Config.LOGTAG,
1449                    account.getJid().asBareJid()
1450                            + "Not processing message. Thread was interrupted");
1451            return;
1452        }
1453        this.messageListener.accept(packet);
1454    }
1455
1456    private void processPresence(final Tag currentTag) throws IOException {
1457        final var packet = processPacket(currentTag, Presence.class);
1458        if (packet.isInvalid()) {
1459            Log.e(
1460                    Config.LOGTAG,
1461                    "encountered invalid presence from='"
1462                            + packet.getFrom()
1463                            + "' to='"
1464                            + packet.getTo()
1465                            + "'");
1466            return;
1467        }
1468        if (Thread.currentThread().isInterrupted()) {
1469            Log.d(
1470                    Config.LOGTAG,
1471                    account.getJid().asBareJid()
1472                            + "Not processing presence. Thread was interrupted");
1473            return;
1474        }
1475        this.presenceListener.accept(packet);
1476    }
1477
1478    private void sendStartTLS() throws IOException {
1479        tagWriter.writeElement(new StartTls());
1480    }
1481
1482    private void switchOverToTls(final Tag currentTag) throws XmlPullParserException, IOException {
1483        tagReader.readElement(currentTag, Proceed.class);
1484        final Socket socket = this.socket;
1485        final SSLSocket sslSocket = upgradeSocketToTls(socket);
1486        this.socket = sslSocket;
1487        this.tagReader.setInputStream(sslSocket.getInputStream());
1488        this.tagWriter.setOutputStream(sslSocket.getOutputStream());
1489        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS connection established");
1490        final boolean quickStart;
1491        try {
1492            quickStart = establishStream(SSLSockets.version(sslSocket));
1493        } catch (final InterruptedException e) {
1494            return;
1495        }
1496        if (quickStart) {
1497            this.quickStartInProgress = true;
1498        }
1499        features.encryptionEnabled = true;
1500        final Tag tag = tagReader.readTag();
1501        if (tag != null && tag.isStart("stream", Namespace.STREAMS)) {
1502            SSLSockets.log(account, sslSocket);
1503            processStream();
1504        } else {
1505            throw new StateChangingException(Account.State.STREAM_OPENING_ERROR);
1506        }
1507        sslSocket.close();
1508    }
1509
1510    private X509Certificate[] certificates(final SSLSession session) throws SSLPeerUnverifiedException {
1511        List<X509Certificate> certs = new ArrayList<>();
1512        for (Certificate certificate : session.getPeerCertificates()) {
1513            if (certificate instanceof X509Certificate) {
1514                certs.add((X509Certificate) certificate);
1515            }
1516        }
1517        return certs.toArray(new X509Certificate[certs.size()]);
1518    }
1519
1520    private SSLSocket upgradeSocketToTls(final Socket socket) throws IOException {
1521        this.dane = false;
1522        final SSLSocketFactory sslSocketFactory;
1523        try {
1524            sslSocketFactory = getSSLSocketFactory(socket.getPort(), (d) -> this.dane = d);
1525        } catch (final NoSuchAlgorithmException | KeyManagementException e) {
1526            throw new StateChangingException(Account.State.TLS_ERROR);
1527        }
1528        final InetAddress address = socket.getInetAddress();
1529        final SSLSocket sslSocket =
1530                (SSLSocket)
1531                        sslSocketFactory.createSocket(
1532                                socket, address.getHostAddress(), socket.getPort(), true);
1533        SSLSockets.setSecurity(sslSocket);
1534        SSLSockets.setHostname(sslSocket, IDN.toASCII(account.getServer()));
1535        SSLSockets.setApplicationProtocol(sslSocket, "xmpp-client");
1536        final XmppDomainVerifier xmppDomainVerifier = new XmppDomainVerifier();
1537        try {
1538            if (!dane && !xmppDomainVerifier.verify(
1539                    account.getServer(), this.verifiedHostname, sslSocket.getSession())) {
1540                Log.d(
1541                        Config.LOGTAG,
1542                        account.getJid().asBareJid()
1543                                + ": TLS certificate domain verification failed");
1544                FileBackend.close(sslSocket);
1545                throw new StateChangingException(Account.State.TLS_ERROR_DOMAIN);
1546            }
1547        } catch (final SSLPeerUnverifiedException e) {
1548            FileBackend.close(sslSocket);
1549            throw new StateChangingException(Account.State.TLS_ERROR);
1550        }
1551        return sslSocket;
1552    }
1553
1554    private void processStreamFeatures(final Tag currentTag) throws IOException {
1555        final var streamFeatures =
1556                tagReader.readElement(
1557                        currentTag, im.conversations.android.xmpp.model.streams.Features.class);
1558        final boolean isSecure = isSecure();
1559        if (streamFeatures.hasExtension(StartTls.class) && !features.encryptionEnabled) {
1560            sendStartTLS();
1561            return;
1562        }
1563        if (isSecure) {
1564            processSecureStreamFeatures(streamFeatures);
1565        } else {
1566            Log.d(
1567                    Config.LOGTAG,
1568                    account.getJid().asBareJid()
1569                            + ": STARTTLS not available "
1570                            + XmlHelper.printElementNames(streamFeatures));
1571            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1572        }
1573    }
1574
1575    private void processSecureStreamFeatures(
1576            final im.conversations.android.xmpp.model.streams.Features streamFeatures)
1577            throws IOException {
1578        this.streamFeatures = streamFeatures;
1579        final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER);
1580        if (this.quickStartInProgress) {
1581            if (streamFeatures.hasStreamFeature(Authentication.class)) {
1582                Log.d(
1583                        Config.LOGTAG,
1584                        account.getJid().asBareJid()
1585                                + ": quick start in progress. ignoring features: "
1586                                + XmlHelper.printElementNames(this.streamFeatures));
1587                if (SaslMechanism.hashedToken(LoginInfo.mechanism(this.loginInfo))) {
1588                    return;
1589                }
1590                if (isFastTokenAvailable(this.streamFeatures.getExtension(Authentication.class))) {
1591                    Log.d(
1592                            Config.LOGTAG,
1593                            account.getJid().asBareJid()
1594                                    + ": fast token available; resetting quick start");
1595                    account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, false);
1596                    mXmppConnectionService.databaseBackend.updateAccount(account);
1597                }
1598                return;
1599            }
1600            Log.d(
1601                    Config.LOGTAG,
1602                    account.getJid().asBareJid()
1603                            + ": server lost support for SASL 2. quick start not possible");
1604            this.account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, false);
1605            mXmppConnectionService.databaseBackend.updateAccount(account);
1606            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1607        }
1608        if (streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE)
1609                && account.isOptionSet(Account.OPTION_REGISTER)) {
1610            register();
1611        } else if (!streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE)
1612                && account.isOptionSet(Account.OPTION_REGISTER)) {
1613            throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED);
1614        } else if (streamFeatures.hasStreamFeature(Authentication.class)
1615                && shouldAuthenticate
1616                && this.loginInfo == null) {
1617            authenticate(SaslMechanism.Version.SASL_2);
1618        } else if (streamFeatures.hasStreamFeature(Mechanisms.class)
1619                && shouldAuthenticate
1620                && this.loginInfo == null) {
1621            authenticate(SaslMechanism.Version.SASL);
1622        } else if (streamFeatures.streamManagement()
1623                && LoginInfo.isSuccess(loginInfo)
1624                && streamId != null
1625                && !inSmacksSession) {
1626            if (Config.EXTENDED_SM_LOGGING) {
1627                Log.d(
1628                        Config.LOGTAG,
1629                        account.getJid().asBareJid()
1630                                + ": resuming after stanza #"
1631                                + stanzasReceived);
1632            }
1633            final var streamId = this.streamId.id;
1634            final var resume = new Resume(streamId, stanzasReceived);
1635            prepareForResume(streamId);
1636            this.tagWriter.writeStanzaAsync(resume);
1637        } else if (needsBinding) {
1638            if (this.streamFeatures.hasChild("bind", Namespace.BIND)
1639                    && LoginInfo.isSuccess(loginInfo)) {
1640                sendBindRequest();
1641            } else {
1642                Log.d(
1643                        Config.LOGTAG,
1644                        account.getJid().asBareJid()
1645                                + ": unable to find bind feature "
1646                                + XmlHelper.printElementNames(this.streamFeatures));
1647                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1648            }
1649        } else {
1650            Log.d(
1651                    Config.LOGTAG,
1652                    account.getJid().asBareJid()
1653                            + ": received NOP stream features: "
1654                            + XmlHelper.printElementNames(this.streamFeatures));
1655        }
1656    }
1657
1658    private void authenticate() throws IOException {
1659        final boolean isSecure = isSecure();
1660        if (isSecure && this.streamFeatures.hasStreamFeature(Authentication.class)) {
1661            authenticate(SaslMechanism.Version.SASL_2);
1662        } else if (isSecure && this.streamFeatures.hasStreamFeature(Mechanisms.class)) {
1663            authenticate(SaslMechanism.Version.SASL);
1664        } else {
1665            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1666        }
1667    }
1668
1669    private boolean isSecure() {
1670        return (features.encryptionEnabled && this.socket instanceof SSLSocket)
1671                || Config.ALLOW_NON_TLS_CONNECTIONS
1672                || account.isDirectToOnion();
1673    }
1674
1675    private void authenticate(final SaslMechanism.Version version) throws IOException {
1676        if (this.loginInfo != null) {
1677            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1678        }
1679        final AuthenticationStreamFeature authElement;
1680        if (version == SaslMechanism.Version.SASL) {
1681            authElement = this.streamFeatures.getExtension(Mechanisms.class);
1682        } else {
1683            authElement = this.streamFeatures.getExtension(Authentication.class);
1684        }
1685        final Collection<String> mechanisms = authElement.getMechanismNames();
1686        final var cbExtension = this.streamFeatures.getExtension(SaslChannelBinding.class);
1687        final Collection<ChannelBinding> channelBindings = ChannelBinding.of(cbExtension);
1688        final SaslMechanism.Factory factory = new SaslMechanism.Factory(account);
1689        final SaslMechanism saslMechanism =
1690                factory.of(mechanisms, channelBindings, version, SSLSockets.version(this.socket));
1691        this.validate(saslMechanism, mechanisms);
1692        final DowngradeProtection downgradeProtection;
1693        if (cbExtension != null) {
1694            downgradeProtection =
1695                    new DowngradeProtection(mechanisms, cbExtension.getChannelBindingTypes());
1696        } else {
1697            downgradeProtection = new DowngradeProtection(mechanisms);
1698        }
1699        if (saslMechanism instanceof ScramMechanism scramMechanism) {
1700            scramMechanism.setDowngradeProtection(downgradeProtection);
1701        }
1702        final boolean quickStartAvailable;
1703        final String firstMessage =
1704                saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket));
1705        final boolean usingFast = SaslMechanism.hashedToken(saslMechanism);
1706        final AuthenticationRequest authenticate;
1707        final LoginInfo loginInfo;
1708        if (version == SaslMechanism.Version.SASL) {
1709            authenticate = new Auth();
1710            if (!Strings.isNullOrEmpty(firstMessage)) {
1711                authenticate.setContent(firstMessage);
1712            }
1713            quickStartAvailable = false;
1714            loginInfo = new LoginInfo(saslMechanism, version, Collections.emptyList());
1715        } else if (version == SaslMechanism.Version.SASL_2) {
1716            final Authentication authentication = (Authentication) authElement;
1717            final var inline = authentication.getInline();
1718            final boolean sm = inline != null && inline.hasExtension(StreamManagement.class);
1719            final HashedToken.Mechanism hashTokenRequest;
1720            if (usingFast) {
1721                hashTokenRequest = null;
1722            } else if (inline != null) {
1723                hashTokenRequest =
1724                        HashedToken.Mechanism.best(
1725                                inline.getFastMechanisms(), SSLSockets.version(this.socket));
1726                // TODO warn or fail early if channel binding priority isn’t high enough compared to
1727                // login mechanism
1728                // ChannelBinding.priority(hashTokenRequest.channelBinding)
1729                //                        <
1730                // ChannelBindingMechanism.getPriority(saslMechanism)
1731            } else {
1732                hashTokenRequest = null;
1733            }
1734            final Collection<String> bindFeatures = Bind2.features(inline);
1735            quickStartAvailable =
1736                    sm
1737                            && bindFeatures != null
1738                            && bindFeatures.containsAll(Bind2.QUICKSTART_FEATURES);
1739            if (bindFeatures != null) {
1740                try {
1741                    mXmppConnectionService.restoredFromDatabaseLatch.await();
1742                } catch (final InterruptedException e) {
1743                    Log.d(
1744                            Config.LOGTAG,
1745                            account.getJid().asBareJid()
1746                                    + ": interrupted while waiting for DB restore during SASL2"
1747                                    + " bind");
1748                    return;
1749                }
1750            }
1751            loginInfo = new LoginInfo(saslMechanism, version, bindFeatures);
1752            this.hashTokenRequest = hashTokenRequest;
1753            authenticate =
1754                    generateAuthenticationRequest(
1755                            firstMessage, usingFast, hashTokenRequest, bindFeatures, sm);
1756        } else {
1757            throw new AssertionError("Missing implementation for " + version);
1758        }
1759        this.loginInfo = loginInfo;
1760        if (account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, quickStartAvailable)) {
1761            mXmppConnectionService.databaseBackend.updateAccount(account);
1762        }
1763
1764        Log.d(
1765                Config.LOGTAG,
1766                account.getJid().toString()
1767                        + ": Authenticating with "
1768                        + version
1769                        + "/"
1770                        + LoginInfo.mechanism(loginInfo).getMechanism());
1771        authenticate.setMechanism(LoginInfo.mechanism(loginInfo));
1772        synchronized (this.mStanzaQueue) {
1773            this.stanzasSentBeforeAuthentication = this.stanzasSent;
1774            tagWriter.writeElement(authenticate);
1775        }
1776    }
1777
1778    private static boolean isFastTokenAvailable(final Authentication authentication) {
1779        final var inline = authentication == null ? null : authentication.getInline();
1780        return inline != null && inline.hasExtension(Fast.class);
1781    }
1782
1783    private void validate(
1784            final @Nullable SaslMechanism saslMechanism, Collection<String> mechanisms)
1785            throws StateChangingException {
1786        if (saslMechanism == null) {
1787            Log.d(
1788                    Config.LOGTAG,
1789                    account.getJid().asBareJid()
1790                            + ": unable to find supported SASL mechanism in "
1791                            + mechanisms);
1792            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1793        }
1794        checkRequireChannelBinding(saslMechanism);
1795        if (SaslMechanism.hashedToken(saslMechanism)) {
1796            return;
1797        }
1798        final int pinnedMechanism = account.getPinnedMechanismPriority();
1799        if (pinnedMechanism > saslMechanism.getPriority()) {
1800            Log.e(
1801                    Config.LOGTAG,
1802                    "Auth failed. Authentication mechanism "
1803                            + saslMechanism.getMechanism()
1804                            + " has lower priority ("
1805                            + saslMechanism.getPriority()
1806                            + ") than pinned priority ("
1807                            + pinnedMechanism
1808                            + "). Possible downgrade attack?");
1809            throw new StateChangingException(Account.State.DOWNGRADE_ATTACK);
1810        }
1811    }
1812
1813    private void checkRequireChannelBinding(@NonNull final SaslMechanism mechanism)
1814            throws StateChangingException {
1815        if (appSettings.isRequireChannelBinding()) {
1816            if (mechanism instanceof ChannelBindingMechanism) {
1817                return;
1818            }
1819            Log.d(Config.LOGTAG, account.getJid() + ": server did not offer channel binding");
1820            throw new StateChangingException(Account.State.CHANNEL_BINDING);
1821        }
1822    }
1823
1824    private void checkAssignedDomainOrThrow(final Jid jid) throws StateChangingException {
1825        if (jid == null) {
1826            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": bind response is missing jid");
1827            throw new StateChangingException(Account.State.BIND_FAILURE);
1828        }
1829        final var current = this.account.getJid().getDomain();
1830        if (jid.getDomain().equals(current)) {
1831            return;
1832        }
1833        Log.d(
1834                Config.LOGTAG,
1835                account.getJid().asBareJid()
1836                        + ": server tried to re-assign domain to "
1837                        + jid.getDomain());
1838        throw new StateChangingException(Account.State.BIND_FAILURE);
1839    }
1840
1841    private void checkAssignedDomain(final Jid jid) {
1842        try {
1843            checkAssignedDomainOrThrow(jid);
1844        } catch (final StateChangingException e) {
1845            throw new StateChangingError(e.state);
1846        }
1847    }
1848
1849    private AuthenticationRequest generateAuthenticationRequest(
1850            final String firstMessage, final boolean usingFast) {
1851        return generateAuthenticationRequest(
1852                firstMessage, usingFast, null, Bind2.QUICKSTART_FEATURES, true);
1853    }
1854
1855    private AuthenticationRequest generateAuthenticationRequest(
1856            final String firstMessage,
1857            final boolean usingFast,
1858            final HashedToken.Mechanism hashedTokenRequest,
1859            final Collection<String> bind,
1860            final boolean inlineStreamManagement) {
1861        final var authenticate = new Authenticate();
1862        if (!Strings.isNullOrEmpty(firstMessage)) {
1863            authenticate.addChild("initial-response").setContent(firstMessage);
1864        }
1865        final var userAgent =
1866                authenticate.addExtension(
1867                        new UserAgent(
1868                                AccountUtils.publicDeviceId(
1869                                        account, appSettings.getInstallationId())));
1870        userAgent.setSoftware(
1871                String.format("%s %s", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME));
1872        if (!PhoneHelper.isEmulator()) {
1873            userAgent.setDevice(String.format("%s %s", Build.MANUFACTURER, Build.MODEL));
1874        }
1875        // do not include bind if 'inlineStreamManagement' is missing and we have a streamId
1876        // (because we would rather just do a normal SM/resume)
1877        final boolean mayAttemptBind = streamId == null || inlineStreamManagement;
1878        if (bind != null && mayAttemptBind) {
1879            authenticate.addChild(generateBindRequest(bind));
1880        }
1881        if (inlineStreamManagement && streamId != null) {
1882            final var streamId = this.streamId.id;
1883            final var resume = new Resume(streamId, stanzasReceived);
1884            prepareForResume(streamId);
1885            authenticate.addExtension(resume);
1886        }
1887        if (hashedTokenRequest != null) {
1888            authenticate.addExtension(new RequestToken(hashedTokenRequest));
1889        }
1890        if (usingFast) {
1891            authenticate.addExtension(new Fast());
1892        }
1893        return authenticate;
1894    }
1895
1896    private void prepareForResume(final String streamId) {
1897        this.mSmCatchupMessageCounter.set(0);
1898        this.mWaitingForSmCatchup.set(true);
1899        this.pendingResumeId.push(streamId);
1900    }
1901
1902    private Bind generateBindRequest(final Collection<String> bindFeatures) {
1903        Log.d(Config.LOGTAG, "inline bind features: " + bindFeatures);
1904        final var bind = new Bind();
1905        bind.setTag(BuildConfig.APP_NAME);
1906        if (bindFeatures.contains(Namespace.CARBONS)) {
1907            bind.addExtension(new im.conversations.android.xmpp.model.carbons.Enable());
1908        }
1909        if (bindFeatures.contains(Namespace.STREAM_MANAGEMENT)) {
1910            bind.addExtension(new Enable());
1911        }
1912        return bind;
1913    }
1914
1915    private void register() {
1916        final String preAuth = account.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN);
1917        if (preAuth != null && features.invite()) {
1918            final Iq preAuthRequest = new Iq(Iq.Type.SET);
1919            preAuthRequest.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth);
1920            sendUnmodifiedIqPacket(
1921                    preAuthRequest,
1922                    (response) -> {
1923                        if (response.getType() == Iq.Type.RESULT) {
1924                            sendRegistryRequest();
1925                        } else {
1926                            final String error = response.getErrorCondition();
1927                            Log.d(
1928                                    Config.LOGTAG,
1929                                    account.getJid().asBareJid()
1930                                            + ": failed to pre auth. "
1931                                            + error);
1932                            throw new StateChangingError(Account.State.REGISTRATION_INVALID_TOKEN);
1933                        }
1934                    },
1935                    true);
1936        } else {
1937            sendRegistryRequest();
1938        }
1939    }
1940
1941    private void sendRegistryRequest() {
1942        final Iq register = new Iq(Iq.Type.GET);
1943        register.query(Namespace.REGISTER);
1944        register.setTo(account.getDomain());
1945        sendUnmodifiedIqPacket(
1946                register,
1947                (packet) -> {
1948                    if (packet.getType() == Iq.Type.TIMEOUT) {
1949                        return;
1950                    }
1951                    if (packet.getType() == Iq.Type.ERROR) {
1952                        throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1953                    }
1954                    final Element query = packet.query(Namespace.REGISTER);
1955                    if (query.hasChild("username") && (query.hasChild("password"))) {
1956                        final Iq register1 = new Iq(Iq.Type.SET);
1957                        final Element username =
1958                                new Element("username").setContent(account.getUsername());
1959                        final Element password =
1960                                new Element("password").setContent(account.getPassword());
1961                        register1.query(Namespace.REGISTER).addChild(username);
1962                        register1.query().addChild(password);
1963                        register1.setFrom(account.getJid().asBareJid());
1964                        sendUnmodifiedIqPacket(register1, this::processRegistrationResponse, true);
1965                    } else if (query.hasChild("x", Namespace.DATA)) {
1966                        final Data data = Data.parse(query.findChild("x", Namespace.DATA));
1967                        final Element blob = query.findChild("data", "urn:xmpp:bob");
1968                        final String id = packet.getId();
1969                        InputStream is;
1970                        if (blob != null) {
1971                            try {
1972                                final String base64Blob = blob.getContent();
1973                                final byte[] strBlob = Base64.decode(base64Blob, Base64.DEFAULT);
1974                                is = new ByteArrayInputStream(strBlob);
1975                            } catch (Exception e) {
1976                                is = null;
1977                            }
1978                        } else {
1979                            final boolean useTor = this.appSettings.isUseTor() || account.isOnion();
1980                            try {
1981                                final String url = data.getValue("url");
1982                                final String fallbackUrl = data.getValue("captcha-fallback-url");
1983                                if (url != null) {
1984                                    is = HttpConnectionManager.open(url, useTor);
1985                                } else if (fallbackUrl != null) {
1986                                    is = HttpConnectionManager.open(fallbackUrl, useTor);
1987                                } else {
1988                                    is = null;
1989                                }
1990                            } catch (final IOException e) {
1991                                Log.d(
1992                                        Config.LOGTAG,
1993                                        account.getJid().asBareJid() + ": unable to fetch captcha",
1994                                        e);
1995                                is = null;
1996                            }
1997                        }
1998
1999                        if (is != null) {
2000                            Bitmap captcha = BitmapFactory.decodeStream(is);
2001                            try {
2002                                if (mXmppConnectionService.displayCaptchaRequest(
2003                                        account, id, data, captcha)) {
2004                                    return;
2005                                }
2006                            } catch (Exception e) {
2007                                throw new StateChangingError(Account.State.REGISTRATION_FAILED);
2008                            }
2009                        }
2010                        throw new StateChangingError(Account.State.REGISTRATION_FAILED);
2011                    } else if (query.hasChild("instructions")
2012                            || query.hasChild("x", Namespace.OOB)) {
2013                        final String instructions = query.findChildContent("instructions");
2014                        final Element oob = query.findChild("x", Namespace.OOB);
2015                        final String url = oob == null ? null : oob.findChildContent("url");
2016                        if (url != null) {
2017                            setAccountCreationFailed(url);
2018                        } else if (instructions != null) {
2019                            final Matcher matcher = Patterns.URI_HTTP.matcher(instructions);
2020                            if (matcher.find()) {
2021                                setAccountCreationFailed(
2022                                        instructions.substring(matcher.start(), matcher.end()));
2023                            }
2024                        }
2025                        throw new StateChangingError(Account.State.REGISTRATION_FAILED);
2026                    }
2027                },
2028                true);
2029    }
2030
2031    public void sendCreateAccountWithCaptchaPacket(final String id, final Data data) {
2032        final Iq request = IqGenerator.generateCreateAccountWithCaptcha(account, id, data);
2033        this.sendUnmodifiedIqPacket(request, this::processRegistrationResponse, true);
2034    }
2035
2036    private void processRegistrationResponse(final Iq response) {
2037        if (response.getType() == Iq.Type.RESULT) {
2038            account.setOption(Account.OPTION_REGISTER, false);
2039            Log.d(
2040                    Config.LOGTAG,
2041                    account.getJid().asBareJid()
2042                            + ": successfully registered new account on server");
2043            throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL);
2044        } else {
2045            final Account.State state = getRegistrationFailedState(response);
2046            throw new StateChangingError(state);
2047        }
2048    }
2049
2050    @NonNull
2051    private static Account.State getRegistrationFailedState(final Iq response) {
2052        final List<String> PASSWORD_TOO_WEAK_MESSAGES =
2053                Arrays.asList("The password is too weak", "Please use a longer password.");
2054        final var error = response.getError();
2055        final var condition = error == null ? null : error.getCondition();
2056        final Account.State state;
2057        if (condition instanceof Condition.Conflict) {
2058            state = Account.State.REGISTRATION_CONFLICT;
2059        } else if (condition instanceof Condition.ResourceConstraint) {
2060            state = Account.State.REGISTRATION_PLEASE_WAIT;
2061        } else if (condition instanceof Condition.NotAcceptable
2062                && PASSWORD_TOO_WEAK_MESSAGES.contains(error.getTextAsString())) {
2063            state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK;
2064        } else {
2065            state = Account.State.REGISTRATION_FAILED;
2066        }
2067        return state;
2068    }
2069
2070    private void setAccountCreationFailed(final String url) {
2071        final HttpUrl httpUrl = url == null ? null : HttpUrl.parse(url);
2072        if (httpUrl != null && httpUrl.isHttps()) {
2073            this.redirectionUrl = httpUrl;
2074            throw new StateChangingError(Account.State.REGISTRATION_WEB);
2075        }
2076        throw new StateChangingError(Account.State.REGISTRATION_FAILED);
2077    }
2078
2079    public HttpUrl getRedirectionUrl() {
2080        return this.redirectionUrl;
2081    }
2082
2083    public void resetEverything() {
2084        resetAttemptCount(true);
2085        resetStreamId();
2086        clearIqCallbacks();
2087        synchronized (this.mStanzaQueue) {
2088            this.stanzasSent = 0;
2089            this.mStanzaQueue.clear();
2090        }
2091        this.redirectionUrl = null;
2092        getManager(DiscoManager.class).clear();
2093        synchronized (this.commands) {
2094            this.commands.clear();
2095        }
2096        this.loginInfo = null;
2097    }
2098
2099    private void sendBindRequest() {
2100        try {
2101            mXmppConnectionService.restoredFromDatabaseLatch.await();
2102        } catch (InterruptedException e) {
2103            Log.d(
2104                    Config.LOGTAG,
2105                    account.getJid().asBareJid()
2106                            + ": interrupted while waiting for DB restore during bind");
2107            return;
2108        }
2109        clearIqCallbacks();
2110        if (account.getJid().isBareJid()) {
2111            account.setResource(createNewResource());
2112        } else {
2113            fixResource(mXmppConnectionService, account);
2114        }
2115        final Iq iq = new Iq(Iq.Type.SET);
2116        final String resource =
2117                Config.USE_RANDOM_RESOURCE_ON_EVERY_BIND
2118                        ? CryptoHelper.random(9)
2119                        : account.getResource();
2120        iq.addExtension(new im.conversations.android.xmpp.model.bind.Bind()).setResource(resource);
2121        this.sendUnmodifiedIqPacket(
2122                iq,
2123                (packet) -> {
2124                    if (packet.getType() == Iq.Type.TIMEOUT) {
2125                        return;
2126                    }
2127                    final var bind =
2128                            packet.getExtension(
2129                                    im.conversations.android.xmpp.model.bind.Bind.class);
2130                    if (bind != null && packet.getType() == Iq.Type.RESULT) {
2131                        isBound = true;
2132                        final Jid assignedJid = bind.getJid();
2133                        checkAssignedDomain(assignedJid);
2134                        if (account.setJid(assignedJid)) {
2135                            Log.d(
2136                                    Config.LOGTAG,
2137                                    account.getJid().asBareJid()
2138                                            + ": jid changed during bind. updating database");
2139                            mXmppConnectionService.databaseBackend.updateAccount(account);
2140                        }
2141                        if (streamFeatures.hasChild("session")
2142                                && !streamFeatures.findChild("session").hasChild("optional")) {
2143                            sendStartSession();
2144                        } else {
2145                            final boolean waitForDisco = enableStreamManagement();
2146                            sendPostBindInitialization(waitForDisco, false);
2147                        }
2148                    } else {
2149                        Log.d(
2150                                Config.LOGTAG,
2151                                account.getJid()
2152                                        + ": disconnecting because of bind failure ("
2153                                        + packet);
2154                        final var error = packet.getError();
2155                        // TODO error.is(Condition)
2156                        if (packet.getType() == Iq.Type.ERROR
2157                                && error != null
2158                                && error.hasChild("conflict")) {
2159                            account.setResource(createNewResource());
2160                        }
2161                        throw new StateChangingError(Account.State.BIND_FAILURE);
2162                    }
2163                },
2164                true);
2165    }
2166
2167    private void clearIqCallbacks() {
2168        final Iq failurePacket = new Iq(Iq.Type.TIMEOUT);
2169        final ArrayList<Consumer<Iq>> callbacks = new ArrayList<>();
2170        synchronized (this.packetCallbacks) {
2171            if (this.packetCallbacks.isEmpty()) {
2172                return;
2173            }
2174            Log.d(
2175                    Config.LOGTAG,
2176                    account.getJid().asBareJid()
2177                            + ": clearing "
2178                            + this.packetCallbacks.size()
2179                            + " iq callbacks");
2180            final var iterator = this.packetCallbacks.values().iterator();
2181            while (iterator.hasNext()) {
2182                final var entry = iterator.next();
2183                if (entry.second.second == null || entry.second.second.cancel(false)) {
2184                    callbacks.add(entry.second.first);
2185                }
2186                iterator.remove();
2187            }
2188        }
2189        for (final var callback : callbacks) {
2190            try {
2191                callback.accept(failurePacket);
2192            } catch (StateChangingError error) {
2193                Log.d(
2194                        Config.LOGTAG,
2195                        account.getJid().asBareJid()
2196                                + ": caught StateChangingError("
2197                                + error.state.toString()
2198                                + ") while clearing callbacks");
2199                // ignore
2200            }
2201        }
2202        Log.d(
2203                Config.LOGTAG,
2204                account.getJid().asBareJid()
2205                        + ": done clearing iq callbacks. "
2206                        + this.packetCallbacks.size()
2207                        + " left");
2208    }
2209
2210    public void sendDiscoTimeout() {
2211        if (mWaitForDisco.compareAndSet(true, false)) {
2212            Log.d(
2213                    Config.LOGTAG,
2214                    account.getJid().asBareJid() + ": finalizing bind after disco timeout");
2215            finalizeBind();
2216        }
2217    }
2218
2219    private void sendStartSession() {
2220        Log.d(
2221                Config.LOGTAG,
2222                account.getJid().asBareJid() + ": sending legacy session to outdated server");
2223        final Iq startSession = new Iq(Iq.Type.SET);
2224        startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session");
2225        this.sendUnmodifiedIqPacket(
2226                startSession,
2227                (packet) -> {
2228                    if (packet.getType() == Iq.Type.RESULT) {
2229                        final boolean waitForDisco = enableStreamManagement();
2230                        sendPostBindInitialization(waitForDisco, false);
2231                    } else if (packet.getType() != Iq.Type.TIMEOUT) {
2232                        throw new StateChangingError(Account.State.SESSION_FAILURE);
2233                    }
2234                },
2235                true);
2236    }
2237
2238    private boolean enableStreamManagement() {
2239        final boolean streamManagement = this.streamFeatures.streamManagement();
2240        if (streamManagement) {
2241            synchronized (this.mStanzaQueue) {
2242                final var enable = new Enable();
2243                tagWriter.writeStanzaAsync(enable);
2244                stanzasSent = 0;
2245                mStanzaQueue.clear();
2246            }
2247            return true;
2248        } else {
2249            return false;
2250        }
2251    }
2252
2253    private void sendPostBindInitialization(
2254            final boolean waitForDisco, final boolean carbonsEnabled) {
2255        features.carbonsEnabled = carbonsEnabled;
2256        features.blockListRequested = false;
2257        getManager(DiscoManager.class).clear();
2258        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery");
2259        mWaitForDisco.set(waitForDisco);
2260        this.lastDiscoStarted = SystemClock.elapsedRealtime();
2261        mXmppConnectionService.scheduleWakeUpCall(
2262                Config.CONNECT_DISCO_TIMEOUT * 1000L, account.getUuid().hashCode());
2263
2264        final var nodeHash = streamFeatures.getCapabilities();
2265        final var serverInfoFuture =
2266                getManager(DiscoManager.class)
2267                        .infoOrCache(Entity.discoItem(account.getDomain()), nodeHash);
2268
2269        final var features = getFeatures();
2270        if (!features.bind2()) {
2271            discoverMamPreferences();
2272        }
2273
2274        final var accountInfoFuture =
2275                getManager(DiscoManager.class)
2276                        .info(Entity.discoItem(account.getJid().asBareJid()), null);
2277
2278        final var itemsFuture =
2279                getManager(DiscoManager.class).itemsWithInfo(Entity.discoItem(account.getDomain()));
2280
2281        final var catchingServerFuture =
2282                Futures.catching(
2283                        serverInfoFuture,
2284                        DiscoManager.CapsHashMismatchException.class,
2285                        input -> {
2286                            Log.d(
2287                                    Config.LOGTAG,
2288                                    account.getJid().asBareJid() + ": error in server caps",
2289                                    input);
2290                            return null;
2291                        },
2292                        MoreExecutors.directExecutor());
2293
2294        Futures.addCallback(
2295                Futures.allAsList(accountInfoFuture, catchingServerFuture),
2296                new FutureCallback<>() {
2297                    @Override
2298                    public void onSuccess(List<Object> result) {
2299                        Log.d(
2300                                Config.LOGTAG,
2301                                account.getJid().asBareJid() + ": advanced stream future done");
2302                        enableAdvancedStreamFeatures();
2303                    }
2304
2305                    @Override
2306                    public void onFailure(@Nullable Throwable throwable) {
2307                        Log.d(
2308                                Config.LOGTAG,
2309                                "could not fetch disco for advanced stream features",
2310                                throwable);
2311                    }
2312                },
2313                MoreExecutors.directExecutor());
2314
2315        if (mWaitForDisco.get()) {
2316            final ListenableFuture<Void> discoComplete =
2317                    Futures.whenAllComplete(serverInfoFuture, accountInfoFuture, itemsFuture)
2318                            .call(() -> null, MoreExecutors.directExecutor());
2319            Futures.addCallback(
2320                    discoComplete,
2321                    new FutureCallback<>() {
2322                        @Override
2323                        public void onSuccess(Void result) {
2324                            if (timeout(serverInfoFuture, accountInfoFuture, itemsFuture)) {
2325                                Log.d(
2326                                        Config.LOGTAG,
2327                                        account.getJid().asBareJid()
2328                                                + ": reached timeout while waiting for disco");
2329                                return;
2330                            }
2331                            if (mWaitForDisco.compareAndSet(true, false)) {
2332                                finalizeBindOrError();
2333                            } else {
2334                                Log.d(
2335                                        Config.LOGTAG,
2336                                        account.getJid().asBareJid()
2337                                                + ": disco complete but bind was already"
2338                                                + " finalized");
2339                            }
2340                        }
2341
2342                        @Override
2343                        public void onFailure(@NonNull Throwable t) {
2344                            Log.d(Config.LOGTAG, "error in disco: ", t);
2345                        }
2346                    },
2347                    MoreExecutors.directExecutor());
2348        } else {
2349            finalizeBind();
2350        }
2351
2352        if (!mWaitForDisco.get()) {
2353            finalizeBind();
2354        }
2355        this.lastSessionStarted = SystemClock.elapsedRealtime();
2356    }
2357
2358    private boolean timeout(final ListenableFuture<?>... futures) {
2359        for (final ListenableFuture<?> future : futures) {
2360            if (future.isDone()) {
2361                try {
2362                    future.get();
2363                } catch (final Exception e) {
2364                    if (Throwables.getRootCause(e) instanceof TimeoutException) {
2365                        return true;
2366                    }
2367                }
2368            }
2369        }
2370        return false;
2371    }
2372
2373    private void discoverMamPreferences() {
2374        final Iq request = new Iq(Iq.Type.GET);
2375        request.addChild("prefs", MessageArchiveService.Version.MAM_2.namespace);
2376        sendIqPacket(
2377                request,
2378                (response) -> {
2379                    if (response.getType() == Iq.Type.RESULT) {
2380                        Element prefs =
2381                                response.findChild(
2382                                        "prefs", MessageArchiveService.Version.MAM_2.namespace);
2383                        isMamPreferenceAlways =
2384                                "always"
2385                                        .equals(
2386                                                prefs == null
2387                                                        ? null
2388                                                        : prefs.getAttribute("default"));
2389                    }
2390                });
2391    }
2392
2393    private void discoverCommands() {
2394        final var future =
2395                getManager(DiscoManager.class).commands(Entity.discoItem(account.getDomain()));
2396        Futures.addCallback(
2397                future,
2398                new FutureCallback<>() {
2399                    @Override
2400                    public void onSuccess(Map<String, Jid> result) {
2401                        synchronized (XmppConnection.this.commands) {
2402                            XmppConnection.this.commands.clear();
2403                            XmppConnection.this.commands.putAll(result);
2404                        }
2405                    }
2406
2407                    @Override
2408                    public void onFailure(@NonNull Throwable throwable) {
2409                        Log.d(
2410                                Config.LOGTAG,
2411                                account.getJid().asBareJid() + ": could not fetch commands",
2412                                throwable);
2413                    }
2414                },
2415                MoreExecutors.directExecutor());
2416    }
2417
2418    public boolean isMamPreferenceAlways() {
2419        return isMamPreferenceAlways;
2420    }
2421
2422    private void finalizeBindOrError() {
2423        try {
2424            finalizeBind();
2425        } catch (final Exception e) {
2426            throw new Error(e);
2427        }
2428    }
2429
2430    private void finalizeBind() {
2431        this.offlineMessagesRetrieved = false;
2432        this.bindListener.run();
2433        this.changeStatusToOnline();
2434    }
2435
2436    private void enableAdvancedStreamFeatures() {
2437        if (getFeatures().blocking() && !features.blockListRequested) {
2438            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Requesting block list");
2439            this.sendIqPacket(getIqGenerator().generateGetBlockList(), unregisteredIqListener);
2440        }
2441        for (final OnAdvancedStreamFeaturesLoaded listener :
2442                advancedStreamFeaturesLoadedListeners) {
2443            listener.onAdvancedStreamFeaturesAvailable(account);
2444        }
2445        if (getFeatures().carbons() && !features.carbonsEnabled) {
2446            sendEnableCarbons();
2447        }
2448        if (getFeatures().commands()) {
2449            discoverCommands();
2450        }
2451    }
2452
2453    private void sendEnableCarbons() {
2454        final Iq iq = new Iq(Iq.Type.SET);
2455        iq.addChild("enable", Namespace.CARBONS);
2456        this.sendIqPacket(
2457                iq,
2458                (packet) -> {
2459                    if (packet.getType() == Iq.Type.RESULT) {
2460                        Log.d(
2461                                Config.LOGTAG,
2462                                account.getJid().asBareJid() + ": successfully enabled carbons");
2463                        features.carbonsEnabled = true;
2464                    } else {
2465                        Log.d(
2466                                Config.LOGTAG,
2467                                account.getJid().asBareJid()
2468                                        + ": could not enable carbons "
2469                                        + packet);
2470                    }
2471                });
2472    }
2473
2474    private void processStreamError(final StreamError streamError) throws IOException {
2475        final var loginInfo = this.loginInfo;
2476        final var isSecureLoggedIn = isSecure() && LoginInfo.isSuccess(loginInfo);
2477        if (isSecureLoggedIn && streamError.hasChild("conflict")) {
2478            if (loginInfo.saslVersion == SaslMechanism.Version.SASL_2) {
2479                this.appSettings.resetInstallationId();
2480            }
2481            account.setResource(createNewResource());
2482            Log.d(
2483                    Config.LOGTAG,
2484                    account.getJid().asBareJid()
2485                            + ": switching resource due to conflict ("
2486                            + account.getResource()
2487                            + ")");
2488            throw new IOException("Closed stream due to resource conflict");
2489        } else if (streamError.hasChild("host-unknown")) {
2490            throw new StateChangingException(Account.State.HOST_UNKNOWN);
2491        } else if (streamError.hasChild("policy-violation")) {
2492            this.lastConnectionStarted = SystemClock.elapsedRealtime();
2493            final String text = streamError.findChildContent("text");
2494            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": policy violation. " + text);
2495            if (isSecureLoggedIn) {
2496                failPendingMessages(text);
2497            }
2498            throw new StateChangingException(Account.State.POLICY_VIOLATION);
2499        } else if (streamError.hasChild("see-other-host")) {
2500            final String seeOtherHost = streamError.findChildContent("see-other-host");
2501            final Resolver.Result currentResolverResult = this.currentResolverResult;
2502            if (Strings.isNullOrEmpty(seeOtherHost) || currentResolverResult == null) {
2503                Log.d(
2504                        Config.LOGTAG,
2505                        account.getJid().asBareJid() + ": stream error " + streamError);
2506                throw new StateChangingException(Account.State.STREAM_ERROR);
2507            }
2508            Log.d(
2509                    Config.LOGTAG,
2510                    account.getJid().asBareJid()
2511                            + ": see other host: "
2512                            + seeOtherHost
2513                            + " "
2514                            + currentResolverResult);
2515            final Resolver.Result seeOtherResult = currentResolverResult.seeOtherHost(seeOtherHost);
2516            if (seeOtherResult != null) {
2517                this.seeOtherHostResolverResult = seeOtherResult;
2518                throw new StateChangingException(Account.State.SEE_OTHER_HOST);
2519            } else {
2520                throw new StateChangingException(Account.State.STREAM_ERROR);
2521            }
2522        } else {
2523            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError);
2524            throw new StateChangingException(Account.State.STREAM_ERROR);
2525        }
2526    }
2527
2528    private void failPendingMessages(final String error) {
2529        synchronized (this.mStanzaQueue) {
2530            for (int i = 0; i < mStanzaQueue.size(); ++i) {
2531                final Stanza stanza = mStanzaQueue.valueAt(i);
2532                if (stanza instanceof im.conversations.android.xmpp.model.stanza.Message packet) {
2533                    final String id = packet.getId();
2534                    final Jid to = packet.getTo();
2535                    mXmppConnectionService.markMessage(
2536                            account, to.asBareJid(), id, Message.STATUS_SEND_FAILED, error);
2537                }
2538            }
2539        }
2540    }
2541
2542    private boolean establishStream(final SSLSockets.Version sslVersion)
2543            throws IOException, InterruptedException {
2544        final boolean secureConnection = sslVersion != SSLSockets.Version.NONE;
2545        final SaslMechanism quickStartMechanism;
2546        if (secureConnection) {
2547            quickStartMechanism =
2548                    SaslMechanism.ensureAvailable(
2549                            account.getQuickStartMechanism(),
2550                            sslVersion,
2551                            appSettings.isRequireChannelBinding());
2552        } else {
2553            quickStartMechanism = null;
2554        }
2555        if (secureConnection
2556                && Config.QUICKSTART_ENABLED
2557                && quickStartMechanism != null
2558                && account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) {
2559            mXmppConnectionService.restoredFromDatabaseLatch.await();
2560            this.loginInfo =
2561                    new LoginInfo(
2562                            quickStartMechanism,
2563                            SaslMechanism.Version.SASL_2,
2564                            Bind2.QUICKSTART_FEATURES);
2565            final boolean usingFast = quickStartMechanism instanceof HashedToken;
2566            final AuthenticationRequest authenticate =
2567                    generateAuthenticationRequest(
2568                            quickStartMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)),
2569                            usingFast);
2570            authenticate.setMechanism(quickStartMechanism);
2571            sendStartStream(true, false);
2572            synchronized (this.mStanzaQueue) {
2573                this.stanzasSentBeforeAuthentication = this.stanzasSent;
2574                tagWriter.writeElement(authenticate);
2575            }
2576            Log.d(
2577                    Config.LOGTAG,
2578                    account.getJid().toString()
2579                            + ": quick start with "
2580                            + quickStartMechanism.getMechanism());
2581            return true;
2582        } else {
2583            sendStartStream(secureConnection, true);
2584            return false;
2585        }
2586    }
2587
2588    private void sendStartStream(final boolean from, final boolean flush) throws IOException {
2589        final Tag stream = Tag.start("stream:stream");
2590        stream.setAttribute("to", account.getServer());
2591        if (from) {
2592            stream.setAttribute("from", account.getJid().asBareJid().toString());
2593        }
2594        stream.setAttribute("version", "1.0");
2595        stream.setAttribute("xml:lang", LocalizedContent.STREAM_LANGUAGE);
2596        stream.setAttribute("xmlns", Namespace.JABBER_CLIENT);
2597        stream.setAttribute("xmlns:stream", Namespace.STREAMS);
2598        tagWriter.writeTag(stream, flush);
2599    }
2600
2601    private static String createNewResource() {
2602        return String.format("%s.%s", BuildConfig.APP_NAME, CryptoHelper.random(3));
2603    }
2604
2605    public ListenableFuture<Iq> sendIqPacket(final Iq request) {
2606        final SettableFuture<Iq> settable = SettableFuture.create();
2607        this.sendIqPacket(
2608                request,
2609                response -> {
2610                    final var type = response.getType();
2611                    switch (type) {
2612                        case RESULT -> settable.set(response);
2613                        case TIMEOUT -> settable.setException(new TimeoutException());
2614                        default -> settable.setException(new IqErrorResponseException(response));
2615                    }
2616                });
2617        return settable;
2618    }
2619
2620    public String sendIqPacket(final Iq packet, final Consumer<Iq> callback) {
2621        return sendIqPacket(packet, callback, null);
2622    }
2623
2624    public String sendIqPacket(final Iq packet, final Consumer<Iq> callback, Long timeout) {
2625        packet.setFrom(account.getJid());
2626        return this.sendUnmodifiedIqPacket(packet, callback, false, timeout);
2627    }
2628
2629    public String sendUnmodifiedIqPacket(final Iq packet, final Consumer<Iq> callback, boolean force) {
2630        return sendUnmodifiedIqPacket(packet, callback, force, null);
2631    }
2632
2633    public synchronized String sendUnmodifiedIqPacket(
2634            final Iq packet, final Consumer<Iq> callback, boolean force, Long timeout) {
2635        // TODO if callback != null verify that type is get or set
2636        if (packet.getId() == null) {
2637            packet.setId(CryptoHelper.random(9));
2638        }
2639        if (callback != null) {
2640            synchronized (this.packetCallbacks) {
2641                ScheduledFuture timeoutFuture = null;
2642                if (timeout != null) {
2643                    timeoutFuture = SCHEDULER.schedule(() -> {
2644                        synchronized (this.packetCallbacks) {
2645                            final var failurePacket = new Iq(Iq.Type.TIMEOUT);
2646                            final var removedCallback = packetCallbacks.remove(packet.getId());
2647                            if (removedCallback != null) removedCallback.second.first.accept(failurePacket);
2648                        }
2649                    }, timeout, TimeUnit.SECONDS);
2650                }
2651                packetCallbacks.put(packet.getId(), new Pair<>(packet, new Pair<>(callback, timeoutFuture)));
2652            }
2653        }
2654        this.sendPacket(packet, force);
2655        return packet.getId();
2656    }
2657
2658    public void sendMessagePacket(final im.conversations.android.xmpp.model.stanza.Message packet) {
2659        this.sendPacket(packet);
2660    }
2661
2662    public void sendPresencePacket(final Presence packet) {
2663        this.sendPacket(packet);
2664    }
2665
2666    private synchronized void sendPacket(final StreamElement packet) {
2667        sendPacket(packet, false);
2668    }
2669
2670    private synchronized void sendPacket(final StreamElement packet, final boolean force) {
2671        if (stanzasSent == Integer.MAX_VALUE) {
2672            resetStreamId();
2673            disconnect(true);
2674            return;
2675        }
2676        synchronized (this.mStanzaQueue) {
2677            if (force || isBound) {
2678                tagWriter.writeStanzaAsync(packet);
2679            } else {
2680                Log.d(
2681                        Config.LOGTAG,
2682                        account.getJid().asBareJid()
2683                                + " do not write stanza to unbound stream "
2684                                + packet.toString());
2685            }
2686            if (packet instanceof Stanza stanza) {
2687                if (this.mStanzaQueue.size() != 0) {
2688                    int currentHighestKey = this.mStanzaQueue.keyAt(this.mStanzaQueue.size() - 1);
2689                    if (currentHighestKey != stanzasSent) {
2690                        throw new AssertionError("Stanza count messed up");
2691                    }
2692                }
2693
2694                ++stanzasSent;
2695                if (Config.EXTENDED_SM_LOGGING) {
2696                    Log.d(
2697                            Config.LOGTAG,
2698                            account.getJid().asBareJid()
2699                                    + ": counting outbound "
2700                                    + packet.getName()
2701                                    + " as #"
2702                                    + stanzasSent);
2703                }
2704                this.mStanzaQueue.append(stanzasSent, stanza);
2705                if (stanza instanceof im.conversations.android.xmpp.model.stanza.Message
2706                        && stanza.getId() != null
2707                        && inSmacksSession) {
2708                    if (Config.EXTENDED_SM_LOGGING) {
2709                        Log.d(
2710                                Config.LOGTAG,
2711                                account.getJid().asBareJid()
2712                                        + ": requesting ack for message stanza #"
2713                                        + stanzasSent);
2714                    }
2715                    tagWriter.writeStanzaAsync(new Request());
2716                }
2717            }
2718        }
2719    }
2720
2721    public void sendPing() {
2722        if (!r()) {
2723            final Iq iq = new Iq(Iq.Type.GET);
2724            iq.setFrom(account.getJid());
2725            iq.addChild("ping", Namespace.PING);
2726            this.sendIqPacket(iq, null);
2727        }
2728        this.lastPingSent = SystemClock.elapsedRealtime();
2729    }
2730
2731    public void setOnJinglePacketReceivedListener(final OnJinglePacketReceived listener) {
2732        this.jingleListener = listener;
2733    }
2734
2735    public void setOnStatusChangedListener(final OnStatusChanged listener) {
2736        this.statusListener = listener;
2737    }
2738
2739    public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) {
2740        this.acknowledgedListener = listener;
2741    }
2742
2743    public void addOnAdvancedStreamFeaturesAvailableListener(
2744            final OnAdvancedStreamFeaturesLoaded listener) {
2745        this.advancedStreamFeaturesLoadedListeners.add(listener);
2746    }
2747
2748    private void forceCloseSocket() {
2749        FileBackend.close(this.socket);
2750        FileBackend.close(this.tagReader);
2751    }
2752
2753    public void interrupt() {
2754        if (this.mThread != null) {
2755            this.mThread.interrupt();
2756        }
2757    }
2758
2759    public void disconnect(final boolean force) {
2760        interrupt();
2761        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": disconnecting force=" + force);
2762        if (force) {
2763            forceCloseSocket();
2764        } else {
2765            final TagWriter currentTagWriter = this.tagWriter;
2766            if (currentTagWriter.isActive()) {
2767                currentTagWriter.finish();
2768                final Socket currentSocket = this.socket;
2769                final CountDownLatch streamCountDownLatch = this.mStreamCountDownLatch;
2770                try {
2771                    currentTagWriter.await(1, TimeUnit.SECONDS);
2772                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": closing stream");
2773                    currentTagWriter.writeTag(Tag.end("stream:stream"));
2774                    if (streamCountDownLatch != null) {
2775                        if (streamCountDownLatch.await(1, TimeUnit.SECONDS)) {
2776                            Log.d(
2777                                    Config.LOGTAG,
2778                                    account.getJid().asBareJid() + ": remote ended stream");
2779                        } else {
2780                            Log.d(
2781                                    Config.LOGTAG,
2782                                    account.getJid().asBareJid()
2783                                            + ": remote has not closed socket. force closing");
2784                        }
2785                    }
2786                } catch (InterruptedException e) {
2787                    Log.d(
2788                            Config.LOGTAG,
2789                            account.getJid().asBareJid()
2790                                    + ": interrupted while gracefully closing stream");
2791                } catch (final IOException e) {
2792                    Log.d(
2793                            Config.LOGTAG,
2794                            account.getJid().asBareJid()
2795                                    + ": io exception during disconnect ("
2796                                    + e.getMessage()
2797                                    + ")");
2798                } finally {
2799                    FileBackend.close(currentSocket);
2800                }
2801            } else {
2802                forceCloseSocket();
2803            }
2804        }
2805    }
2806
2807    private void resetStreamId() {
2808        this.pendingResumeId.clear();
2809        this.streamId = null;
2810        this.boundStreamFeatures = null;
2811    }
2812
2813    public <M extends AbstractManager> M getManager(final Class<M> clazz) {
2814        return this.managers.getInstance(clazz);
2815    }
2816
2817    private List<Entry<Jid, InfoQuery>> findDiscoItemsByFeature(final String feature) {
2818        final List<Entry<Jid, InfoQuery>> items = new ArrayList<>();
2819        for (final Entry<Jid, InfoQuery> cursor :
2820                getManager(DiscoManager.class).getServerItems().entrySet()) {
2821            if (cursor.getValue().getFeatureStrings().contains(feature)) {
2822                items.add(cursor);
2823            }
2824        }
2825        return items;
2826    }
2827
2828    public Entry<Jid, InfoQuery> getServiceDiscoveryResultByFeature(final String feature) {
2829        return Iterables.getFirst(findDiscoItemsByFeature(feature), null);
2830    }
2831
2832    public Jid findDiscoItemByFeature(final String feature) {
2833        final var items = findDiscoItemsByFeature(feature);
2834        if (items.isEmpty()) {
2835            return null;
2836        }
2837        return Iterables.getFirst(items, null).getKey();
2838    }
2839
2840    public boolean r() {
2841        if (getFeatures().sm()) {
2842            this.tagWriter.writeStanzaAsync(new Request());
2843            return true;
2844        } else {
2845            return false;
2846        }
2847    }
2848
2849    public List<String> getMucServersWithholdAccount() {
2850        final List<String> servers = getMucServers();
2851        servers.remove(account.getDomain().toString());
2852        return servers;
2853    }
2854
2855    public List<String> getMucServers() {
2856        List<String> servers = new ArrayList<>();
2857        for (final Entry<Jid, InfoQuery> entry :
2858                getManager(DiscoManager.class).getServerItems().entrySet()) {
2859            final var value = entry.getValue();
2860            if (value.getFeatureStrings().contains("http://jabber.org/protocol/muc")
2861                    && value.hasIdentityWithCategoryAndType("conference", "text")
2862                    && !value.getFeatureStrings().contains("jabber:iq:gateway")
2863                    && !value.hasIdentityWithCategoryAndType("conference", "irc")) {
2864                servers.add(entry.getKey().toString());
2865            }
2866        }
2867        return servers;
2868    }
2869
2870    public String getMucServer() {
2871        return Iterables.getFirst(getMucServers(), null);
2872    }
2873
2874    public int getTimeToNextAttempt(final boolean aggressive) {
2875        final int interval;
2876        if (aggressive) {
2877            interval = Math.min((int) (3 * Math.pow(1.3, attempt)), 60);
2878        } else {
2879            final int additionalTime =
2880                    account.getLastErrorStatus() == Account.State.POLICY_VIOLATION ? 3 : 0;
2881            interval = Math.min((int) (25 * Math.pow(1.3, (additionalTime + attempt))), 300);
2882        }
2883        final var connectionDuration = Ints.saturatedCast(getConnectionDuration() / 1000);
2884        return interval - connectionDuration;
2885    }
2886
2887    public int getAttempt() {
2888        return this.attempt;
2889    }
2890
2891    public Features getFeatures() {
2892        return this.features;
2893    }
2894
2895    public long getLastSessionEstablished() {
2896        final long diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
2897        return System.currentTimeMillis() - diff;
2898    }
2899
2900    public long getConnectionDuration() {
2901        return SystemClock.elapsedRealtime() - this.lastConnectionStarted;
2902    }
2903
2904    public long getDiscoDuration() {
2905        return SystemClock.elapsedRealtime() - this.lastDiscoStarted;
2906    }
2907
2908    public long getLastPingSent() {
2909        return this.lastPingSent;
2910    }
2911
2912    public long getLastPacketReceived() {
2913        return this.lastPacketReceived;
2914    }
2915
2916    public void sendActive() {
2917        this.sendPacket(new Active());
2918    }
2919
2920    public void sendInactive() {
2921        this.sendPacket(new Inactive());
2922    }
2923
2924    public void resetAttemptCount(boolean resetConnectTime) {
2925        this.attempt = 0;
2926        if (resetConnectTime) {
2927            this.lastConnectionStarted = 0;
2928        }
2929    }
2930
2931    public void setInteractive(boolean interactive) {
2932        this.mInteractive = interactive;
2933    }
2934
2935    private IqGenerator getIqGenerator() {
2936        return mXmppConnectionService.getIqGenerator();
2937    }
2938
2939    public void trackOfflineMessageRetrieval(boolean trackOfflineMessageRetrieval) {
2940        if (trackOfflineMessageRetrieval) {
2941            final Iq iqPing = new Iq(Iq.Type.GET);
2942            iqPing.addChild("ping", Namespace.PING);
2943            this.sendIqPacket(
2944                    iqPing,
2945                    (response) -> {
2946                        Log.d(
2947                                Config.LOGTAG,
2948                                account.getJid().asBareJid()
2949                                        + ": got ping response after sending initial presence");
2950                        XmppConnection.this.offlineMessagesRetrieved = true;
2951                    });
2952        } else {
2953            this.offlineMessagesRetrieved = true;
2954        }
2955    }
2956
2957    public boolean isOfflineMessagesRetrieved() {
2958        return this.offlineMessagesRetrieved;
2959    }
2960
2961    public void fetchRoster() {
2962        final Iq iqPacket = new Iq(Iq.Type.GET);
2963        final var version = account.getRosterVersion();
2964        if (Strings.isNullOrEmpty(account.getRosterVersion())) {
2965            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching roster");
2966        } else {
2967            Log.d(
2968                    Config.LOGTAG,
2969                    account.getJid().asBareJid() + ": fetching roster version " + version);
2970        }
2971        iqPacket.query(Namespace.ROSTER).setAttribute("ver", version);
2972        sendIqPacket(iqPacket, unregisteredIqListener);
2973    }
2974
2975    public void triggerConnectionTimeout() {
2976        final var duration = getConnectionDuration();
2977        Log.d(
2978                Config.LOGTAG,
2979                account.getJid().asBareJid() + ": connection timeout after " + duration + "ms");
2980
2981        // last connection time gets reset so time to next attempt is calculated correctly
2982        this.lastConnectionStarted = SystemClock.elapsedRealtime();
2983
2984        // interrupt needs to be called before status change; otherwise we interrupt the newly
2985        // created thread
2986        this.interrupt();
2987        this.forceCloseSocket();
2988        this.changeStatus(Account.State.CONNECTION_TIMEOUT);
2989    }
2990
2991    public Account getAccount() {
2992        return this.account;
2993    }
2994
2995    private class MyKeyManager implements X509KeyManager {
2996        @Override
2997        public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
2998            return account.getPrivateKeyAlias();
2999        }
3000
3001        @Override
3002        public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
3003            return null;
3004        }
3005
3006        @Override
3007        public X509Certificate[] getCertificateChain(String alias) {
3008            Log.d(Config.LOGTAG, "getting certificate chain");
3009            try {
3010                return KeyChain.getCertificateChain(mXmppConnectionService, alias);
3011            } catch (final Exception e) {
3012                Log.d(Config.LOGTAG, "could not get certificate chain", e);
3013                return new X509Certificate[0];
3014            }
3015        }
3016
3017        @Override
3018        public String[] getClientAliases(String s, Principal[] principals) {
3019            final String alias = account.getPrivateKeyAlias();
3020            return alias != null ? new String[] {alias} : new String[0];
3021        }
3022
3023        @Override
3024        public String[] getServerAliases(String s, Principal[] principals) {
3025            return new String[0];
3026        }
3027
3028        @Override
3029        public PrivateKey getPrivateKey(String alias) {
3030            try {
3031                return KeyChain.getPrivateKey(mXmppConnectionService, alias);
3032            } catch (Exception e) {
3033                return null;
3034            }
3035        }
3036    }
3037
3038    private static class LoginInfo {
3039        public final SaslMechanism saslMechanism;
3040        public final SaslMechanism.Version saslVersion;
3041        public final List<String> inlineBindFeatures;
3042        public final AtomicBoolean success = new AtomicBoolean(false);
3043
3044        private LoginInfo(
3045                final SaslMechanism saslMechanism,
3046                final SaslMechanism.Version saslVersion,
3047                final Collection<String> inlineBindFeatures) {
3048            Preconditions.checkNotNull(saslMechanism, "SASL Mechanism must not be null");
3049            Preconditions.checkNotNull(saslVersion, "SASL version must not be null");
3050            this.saslMechanism = saslMechanism;
3051            this.saslVersion = saslVersion;
3052            this.inlineBindFeatures =
3053                    inlineBindFeatures == null
3054                            ? Collections.emptyList()
3055                            : ImmutableList.copyOf(inlineBindFeatures);
3056        }
3057
3058        public static SaslMechanism mechanism(final LoginInfo loginInfo) {
3059            return loginInfo == null ? null : loginInfo.saslMechanism;
3060        }
3061
3062        public void success(final String challenge, final SSLSocket sslSocket)
3063                throws SaslMechanism.AuthenticationException {
3064            if (Thread.currentThread().isInterrupted()) {
3065                throw new SaslMechanism.AuthenticationException("Race condition during auth");
3066            }
3067            final var response = this.saslMechanism.getResponse(challenge, sslSocket);
3068            if (!Strings.isNullOrEmpty(response)) {
3069                throw new SaslMechanism.AuthenticationException(
3070                        "processing success yielded another response");
3071            }
3072            if (this.success.compareAndSet(false, true)) {
3073                return;
3074            }
3075            throw new SaslMechanism.AuthenticationException("Process 'success' twice");
3076        }
3077
3078        public static boolean isSuccess(final LoginInfo loginInfo) {
3079            return loginInfo != null && loginInfo.success.get();
3080        }
3081    }
3082
3083    private static class StreamId {
3084        public final String id;
3085        public final Resolver.Result location;
3086
3087        private StreamId(String id, Resolver.Result location) {
3088            this.id = id;
3089            this.location = location;
3090        }
3091
3092        @NonNull
3093        @Override
3094        public String toString() {
3095            return MoreObjects.toStringHelper(this)
3096                    .add("id", id)
3097                    .add("location", location)
3098                    .toString();
3099        }
3100    }
3101
3102    private static class StateChangingError extends Error {
3103        private final Account.State state;
3104
3105        public StateChangingError(Account.State state) {
3106            this.state = state;
3107        }
3108    }
3109
3110    private static class StateChangingException extends IOException {
3111        private final Account.State state;
3112
3113        public StateChangingException(Account.State state) {
3114            this.state = state;
3115        }
3116    }
3117
3118    public abstract static class Delegate {
3119
3120        protected final XmppConnectionService context;
3121        protected final XmppConnection connection;
3122
3123        protected Delegate(final XmppConnectionService context, final XmppConnection connection) {
3124            this.context = context;
3125            this.connection = connection;
3126        }
3127
3128        protected Account getAccount() {
3129            return connection.account;
3130        }
3131
3132        protected DatabaseBackend getDatabase() {
3133            return DatabaseBackend.getInstance(context);
3134        }
3135
3136        protected <T extends AbstractManager> T getManager(final Class<T> type) {
3137            return connection.getManager(type);
3138        }
3139    }
3140
3141    public class Features {
3142        XmppConnection connection;
3143        private boolean carbonsEnabled = false;
3144        private boolean encryptionEnabled = false;
3145        private boolean blockListRequested = false;
3146
3147        public Features(final XmppConnection connection) {
3148            this.connection = connection;
3149        }
3150
3151        private boolean hasDiscoFeature(final Jid server, final String feature) {
3152            final var infoQuery = getManager(DiscoManager.class).get(server);
3153            return infoQuery != null && infoQuery.getFeatureStrings().contains(feature);
3154        }
3155
3156        public boolean carbons() {
3157            return hasDiscoFeature(account.getDomain(), Namespace.CARBONS);
3158        }
3159
3160        public boolean commands() {
3161            return hasDiscoFeature(account.getDomain(), Namespace.COMMANDS);
3162        }
3163
3164        public boolean easyOnboardingInvites() {
3165            synchronized (commands) {
3166                return commands.containsKey(Namespace.EASY_ONBOARDING_INVITE);
3167            }
3168        }
3169
3170        public boolean bookmarksConversion() {
3171            return hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS_CONVERSION)
3172                    && pepPublishOptions();
3173        }
3174
3175        public boolean blocking() {
3176            return hasDiscoFeature(account.getDomain(), Namespace.BLOCKING);
3177        }
3178
3179        public boolean spamReporting() {
3180            return hasDiscoFeature(account.getDomain(), Namespace.REPORTING);
3181        }
3182
3183        public boolean flexibleOfflineMessageRetrieval() {
3184            return hasDiscoFeature(
3185                    account.getDomain(), Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL);
3186        }
3187
3188        public boolean register() {
3189            return hasDiscoFeature(account.getDomain(), Namespace.REGISTER);
3190        }
3191
3192        public boolean invite() {
3193            return connection.streamFeatures != null
3194                    && connection.streamFeatures.hasChild("register", Namespace.INVITE);
3195        }
3196
3197        public boolean sm() {
3198            return streamId != null
3199                    || (connection.streamFeatures != null
3200                            && connection.streamFeatures.streamManagement());
3201        }
3202
3203        public boolean csi() {
3204            return connection.streamFeatures != null
3205                    && connection.streamFeatures.clientStateIndication();
3206        }
3207
3208        public boolean pep() {
3209            final var infoQuery = getManager(DiscoManager.class).get(account.getJid().asBareJid());
3210            return infoQuery != null && infoQuery.hasIdentityWithCategoryAndType("pubsub", "pep");
3211        }
3212
3213        public boolean pepPersistent() {
3214            final var infoQuery = getManager(DiscoManager.class).get(account.getJid().asBareJid());
3215            return infoQuery != null
3216                    && infoQuery
3217                            .getFeatureStrings()
3218                            .contains("http://jabber.org/protocol/pubsub#persistent-items");
3219        }
3220
3221        public boolean bind2() {
3222            final var loginInfo = XmppConnection.this.loginInfo;
3223            return loginInfo != null && !loginInfo.inlineBindFeatures.isEmpty();
3224        }
3225
3226        public boolean sasl2() {
3227            final var loginInfo = XmppConnection.this.loginInfo;
3228            return loginInfo != null && loginInfo.saslVersion == SaslMechanism.Version.SASL_2;
3229        }
3230
3231        public String loginMechanism() {
3232            final var loginInfo = XmppConnection.this.loginInfo;
3233            return loginInfo == null ? null : loginInfo.saslMechanism.getMechanism();
3234        }
3235
3236        public boolean pepPublishOptions() {
3237            return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUBSUB_PUBLISH_OPTIONS);
3238        }
3239
3240        public boolean pepConfigNodeMax() {
3241            return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUBSUB_CONFIG_NODE_MAX);
3242        }
3243
3244        public boolean pepOmemoWhitelisted() {
3245            return hasDiscoFeature(
3246                    account.getJid().asBareJid(), AxolotlService.PEP_OMEMO_WHITELISTED);
3247        }
3248
3249        public boolean mam() {
3250            return MessageArchiveService.Version.has(getAccountFeatures());
3251        }
3252
3253        public Collection<String> getAccountFeatures() {
3254            final var infoQuery = getManager(DiscoManager.class).get(account.getJid().asBareJid());
3255            return infoQuery == null ? Collections.emptyList() : infoQuery.getFeatureStrings();
3256        }
3257
3258        public boolean push() {
3259            return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUSH)
3260                    || hasDiscoFeature(account.getDomain(), Namespace.PUSH);
3261        }
3262
3263        public boolean rosterVersioning() {
3264            return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver");
3265        }
3266
3267        public void setBlockListRequested(boolean value) {
3268            this.blockListRequested = value;
3269        }
3270
3271        public HttpUrl getServiceOutageStatus() {
3272            final var disco = getManager(DiscoManager.class).get(account.getDomain());
3273            if (disco == null) {
3274                return null;
3275            }
3276            final var address =
3277                    disco.getServiceDiscoveryExtension(
3278                            Namespace.SERVICE_OUTAGE_STATUS, "external-status-addresses");
3279            if (Strings.isNullOrEmpty(address)) {
3280                return null;
3281            }
3282            return HttpUrl.parse(address);
3283        }
3284
3285        public boolean httpUpload(long fileSize) {
3286            if (Config.DISABLE_HTTP_UPLOAD) {
3287                return false;
3288            }
3289            final var result = getServiceDiscoveryResultByFeature(Namespace.HTTP_UPLOAD);
3290            if (result == null) {
3291                return false;
3292            }
3293            final long maxSize;
3294            try {
3295                maxSize =
3296                        Long.parseLong(
3297                                result.getValue()
3298                                        .getServiceDiscoveryExtension(
3299                                                Namespace.HTTP_UPLOAD, "max-file-size"));
3300            } catch (final Exception e) {
3301                return true;
3302            }
3303            if (fileSize <= maxSize) {
3304                return true;
3305            } else {
3306                Log.d(
3307                        Config.LOGTAG,
3308                        account.getJid().asBareJid()
3309                                + ": http upload is not available for files with"
3310                                + " size "
3311                                + fileSize
3312                                + " (max is "
3313                                + maxSize
3314                                + ")");
3315                return false;
3316            }
3317        }
3318
3319        public long getMaxHttpUploadSize() {
3320            final var result = getServiceDiscoveryResultByFeature(Namespace.HTTP_UPLOAD);
3321            if (result == null) {
3322                return -1;
3323            }
3324            try {
3325                return Long.parseLong(
3326                        result.getValue()
3327                                .getServiceDiscoveryExtension(
3328                                        Namespace.HTTP_UPLOAD, "max-file-size"));
3329            } catch (final Exception e) {
3330                return -1;
3331                // ignored
3332            }
3333        }
3334
3335        public boolean stanzaIds() {
3336            return hasDiscoFeature(account.getJid().asBareJid(), Namespace.STANZA_IDS);
3337        }
3338
3339        public boolean bookmarks2() {
3340            return pepPublishOptions()
3341                    && pepConfigNodeMax()
3342                    && hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT);
3343        }
3344
3345        public boolean externalServiceDiscovery() {
3346            return hasDiscoFeature(account.getDomain(), Namespace.EXTERNAL_SERVICE_DISCOVERY);
3347        }
3348
3349        public boolean mds() {
3350            return pepPublishOptions()
3351                    && pepConfigNodeMax()
3352                    && Config.MESSAGE_DISPLAYED_SYNCHRONIZATION;
3353        }
3354
3355        public boolean mdsServerAssist() {
3356            return hasDiscoFeature(account.getJid().asBareJid(), Namespace.MDS_DISPLAYED);
3357        }
3358    }
3359}