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