XmppConnection.java

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