XmppConnection.java

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