XmppConnection.java

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