XmppConnection.java

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