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