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