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