XmppConnection.java

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