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