XmppConnection.java

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