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