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