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