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