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")) {
608 final Element resumed = tagReader.readElement(nextTag);
609 processResumed(resumed);
610 } else if (nextTag.isStart("r")) {
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")) {
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")) {
665 final Element failed = tagReader.readElement(nextTag);
666 processFailed(failed, true);
667 } else if (nextTag.isStart("iq")) {
668 processIq(nextTag);
669 } else if (nextTag.isStart("message")) {
670 processMessage(nextTag);
671 } else if (nextTag.isStart("presence")) {
672 processPresence(nextTag);
673 }
674 nextTag = tagReader.readTag();
675 }
676 if (nextTag != null && nextTag.isEnd("stream")) {
677 streamCountDownLatch.countDown();
678 }
679 }
680
681 private void processChallenge(final Element challenge) throws IOException {
682 final SaslMechanism.Version version;
683 try {
684 version = SaslMechanism.Version.of(challenge);
685 } catch (final IllegalArgumentException e) {
686 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
687 }
688 final Element response;
689 if (version == SaslMechanism.Version.SASL) {
690 response = new Element("response", Namespace.SASL);
691 } else if (version == SaslMechanism.Version.SASL_2) {
692 response = new Element("response", Namespace.SASL_2);
693 } else {
694 throw new AssertionError("Missing implementation for " + version);
695 }
696 try {
697 response.setContent(saslMechanism.getResponse(challenge.getContent(), sslSocketOrNull(socket)));
698 } catch (final SaslMechanism.AuthenticationException e) {
699 // TODO: Send auth abort tag.
700 Log.e(Config.LOGTAG, e.toString());
701 throw new StateChangingException(Account.State.UNAUTHORIZED);
702 }
703 tagWriter.writeElement(response);
704 }
705
706 private boolean processSuccess(final Element success)
707 throws IOException, XmlPullParserException {
708 final SaslMechanism.Version version;
709 try {
710 version = SaslMechanism.Version.of(success);
711 } catch (final IllegalArgumentException e) {
712 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
713 }
714 final SaslMechanism currentSaslMechanism = this.saslMechanism;
715 if (currentSaslMechanism == null) {
716 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
717 }
718 final String challenge;
719 if (version == SaslMechanism.Version.SASL) {
720 challenge = success.getContent();
721 } else if (version == SaslMechanism.Version.SASL_2) {
722 challenge = success.findChildContent("additional-data");
723 } else {
724 throw new AssertionError("Missing implementation for " + version);
725 }
726 try {
727 currentSaslMechanism.getResponse(challenge, sslSocketOrNull(socket));
728 } catch (final SaslMechanism.AuthenticationException e) {
729 Log.e(Config.LOGTAG, String.valueOf(e));
730 throw new StateChangingException(Account.State.UNAUTHORIZED);
731 }
732 Log.d(
733 Config.LOGTAG,
734 account.getJid().asBareJid().toString() + ": logged in (using " + version + ")");
735 if (SaslMechanism.pin(currentSaslMechanism)) {
736 account.setPinnedMechanism(currentSaslMechanism);
737 }
738 if (version == SaslMechanism.Version.SASL_2) {
739 final String authorizationIdentifier =
740 success.findChildContent("authorization-identifier");
741 final Jid authorizationJid;
742 try {
743 authorizationJid =
744 Strings.isNullOrEmpty(authorizationIdentifier)
745 ? null
746 : Jid.ofEscaped(authorizationIdentifier);
747 } catch (final IllegalArgumentException e) {
748 Log.d(
749 Config.LOGTAG,
750 account.getJid().asBareJid()
751 + ": SASL 2.0 authorization identifier was not a valid jid");
752 throw new StateChangingException(Account.State.BIND_FAILURE);
753 }
754 if (authorizationJid == null) {
755 throw new StateChangingException(Account.State.BIND_FAILURE);
756 }
757 Log.d(
758 Config.LOGTAG,
759 account.getJid().asBareJid()
760 + ": SASL 2.0 authorization identifier was "
761 + authorizationJid);
762 if (!account.getJid().getDomain().equals(authorizationJid.getDomain())) {
763 Log.d(
764 Config.LOGTAG,
765 account.getJid().asBareJid()
766 + ": server tried to re-assign domain to "
767 + authorizationJid.getDomain());
768 throw new StateChangingError(Account.State.BIND_FAILURE);
769 }
770 if (authorizationJid.isFullJid() && account.setJid(authorizationJid)) {
771 Log.d(
772 Config.LOGTAG,
773 account.getJid().asBareJid()
774 + ": jid changed during SASL 2.0. updating database");
775 }
776 final Element bound = success.findChild("bound", Namespace.BIND2);
777 final Element resumed = success.findChild("resumed", Namespace.STREAM_MANAGEMENT);
778 final Element failed = success.findChild("failed", Namespace.STREAM_MANAGEMENT);
779 final Element tokenWrapper = success.findChild("token", Namespace.FAST);
780 final String token = tokenWrapper == null ? null : tokenWrapper.getAttribute("token");
781 if (bound != null && resumed != null) {
782 Log.d(
783 Config.LOGTAG,
784 account.getJid().asBareJid()
785 + ": server sent bound and resumed in SASL2 success");
786 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
787 }
788 if (resumed != null && streamId != null) {
789 if (this.boundStreamFeatures != null) {
790 this.streamFeatures = this.boundStreamFeatures;
791 Log.d(Config.LOGTAG, "putting previous stream features back in place: " + XmlHelper.printElementNames(this.boundStreamFeatures));
792 }
793 processResumed(resumed);
794 } else if (failed != null) {
795 processFailed(failed, false); // wait for new stream features
796 }
797 if (bound != null) {
798 clearIqCallbacks();
799 this.isBound = true;
800 processNopStreamFeatures();
801 this.boundStreamFeatures = this.streamFeatures;
802 final Element streamManagementEnabled =
803 bound.findChild("enabled", Namespace.STREAM_MANAGEMENT);
804 final Element carbonsEnabled = bound.findChild("enabled", Namespace.CARBONS);
805 final boolean waitForDisco;
806 if (streamManagementEnabled != null) {
807 resetOutboundStanzaQueue();
808 processEnabled(streamManagementEnabled);
809 waitForDisco = true;
810 } else {
811 //if we did not enable stream management in bind do it now
812 waitForDisco = enableStreamManagement();
813 }
814 if (carbonsEnabled != null) {
815 Log.d(
816 Config.LOGTAG,
817 account.getJid().asBareJid() + ": successfully enabled carbons");
818 features.carbonsEnabled = true;
819 }
820 sendPostBindInitialization(waitForDisco, carbonsEnabled != null);
821 }
822 final HashedToken.Mechanism tokenMechanism;
823 if (SaslMechanism.hashedToken(currentSaslMechanism)) {
824 tokenMechanism = ((HashedToken) currentSaslMechanism).getTokenMechanism();
825 } else if (this.hashTokenRequest != null) {
826 tokenMechanism = this.hashTokenRequest;
827 } else {
828 tokenMechanism = null;
829 }
830 if (tokenMechanism != null && !Strings.isNullOrEmpty(token)) {
831 if (ChannelBinding.priority(tokenMechanism.channelBinding) >= ChannelBindingMechanism.getPriority(currentSaslMechanism)) {
832 this.account.setFastToken(tokenMechanism, token);
833 Log.d(
834 Config.LOGTAG,
835 account.getJid().asBareJid() + ": storing hashed token " + tokenMechanism);
836 } else {
837 Log.d(Config.LOGTAG,account.getJid().asBareJid()+": not accepting hashed token "+ tokenMechanism.name()+" for log in mechanism "+currentSaslMechanism.getMechanism());
838 this.account.resetFastToken();
839 }
840 } else if (this.hashTokenRequest != null) {
841 Log.w(
842 Config.LOGTAG,
843 account.getJid().asBareJid()
844 + ": no response to our hashed token request "
845 + this.hashTokenRequest);
846 }
847 }
848 mXmppConnectionService.databaseBackend.updateAccount(account);
849 this.quickStartInProgress = false;
850 if (version == SaslMechanism.Version.SASL) {
851 tagReader.reset();
852 sendStartStream(false, true);
853 final Tag tag = tagReader.readTag();
854 if (tag != null && tag.isStart("stream", Namespace.STREAMS)) {
855 processStream();
856 return true;
857 } else {
858 throw new StateChangingException(Account.State.STREAM_OPENING_ERROR);
859 }
860 } else {
861 return false;
862 }
863 }
864
865 private void resetOutboundStanzaQueue() {
866 synchronized (this.mStanzaQueue) {
867 final List<AbstractAcknowledgeableStanza> intermediateStanzas = new ArrayList<>();
868 if (Config.EXTENDED_SM_LOGGING) {
869 Log.d(
870 Config.LOGTAG,
871 account.getJid().asBareJid()
872 + ": stanzas sent before auth: "
873 + this.stanzasSentBeforeAuthentication);
874 }
875 for (int i = this.stanzasSentBeforeAuthentication + 1; i <= this.stanzasSent; ++i) {
876 final AbstractAcknowledgeableStanza stanza = this.mStanzaQueue.get(i);
877 if (stanza != null) {
878 intermediateStanzas.add(stanza);
879 }
880 }
881 this.mStanzaQueue.clear();
882 for (int i = 0; i < intermediateStanzas.size(); ++i) {
883 this.mStanzaQueue.put(i, intermediateStanzas.get(i));
884 }
885 this.stanzasSent = intermediateStanzas.size();
886 if (Config.EXTENDED_SM_LOGGING) {
887 Log.d(
888 Config.LOGTAG,
889 account.getJid().asBareJid()
890 + ": resetting outbound stanza queue to "
891 + this.stanzasSent);
892 }
893 }
894 }
895
896 private void processNopStreamFeatures() throws IOException {
897 final Tag tag = tagReader.readTag();
898 if (tag != null && tag.isStart("features", Namespace.STREAMS)) {
899 this.streamFeatures = tagReader.readElement(tag);
900 Log.d(
901 Config.LOGTAG,
902 account.getJid().asBareJid()
903 + ": processed NOP stream features after success: "
904 + XmlHelper.printElementNames(this.streamFeatures));
905 } else {
906 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received " + tag);
907 Log.d(
908 Config.LOGTAG,
909 account.getJid().asBareJid()
910 + ": server did not send stream features after SASL2 success");
911 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
912 }
913 }
914
915 private void processFailure(final Element failure) throws IOException {
916 final SaslMechanism.Version version;
917 try {
918 version = SaslMechanism.Version.of(failure);
919 } catch (final IllegalArgumentException e) {
920 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
921 }
922 Log.d(Config.LOGTAG, failure.toString());
923 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version);
924 if (SaslMechanism.hashedToken(this.saslMechanism)) {
925 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resetting token");
926 account.resetFastToken();
927 mXmppConnectionService.databaseBackend.updateAccount(account);
928 }
929 if (failure.hasChild("temporary-auth-failure")) {
930 throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE);
931 } else if (failure.hasChild("account-disabled")) {
932 final String text = failure.findChildContent("text");
933 if (Strings.isNullOrEmpty(text)) {
934 throw new StateChangingException(Account.State.UNAUTHORIZED);
935 }
936 final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(text);
937 if (matcher.find()) {
938 final HttpUrl url;
939 try {
940 url = HttpUrl.get(text.substring(matcher.start(), matcher.end()));
941 } catch (final IllegalArgumentException e) {
942 throw new StateChangingException(Account.State.UNAUTHORIZED);
943 }
944 if (url.isHttps()) {
945 this.redirectionUrl = url;
946 throw new StateChangingException(Account.State.PAYMENT_REQUIRED);
947 }
948 }
949 }
950 if (SaslMechanism.hashedToken(this.saslMechanism)) {
951 Log.d(
952 Config.LOGTAG,
953 account.getJid().asBareJid()
954 + ": fast authentication failed. falling back to regular authentication");
955 authenticate();
956 } else {
957 throw new StateChangingException(Account.State.UNAUTHORIZED);
958 }
959 }
960
961 private static SSLSocket sslSocketOrNull(final Socket socket) {
962 if (socket instanceof SSLSocket) {
963 return (SSLSocket) socket;
964 } else {
965 return null;
966 }
967 }
968
969 private void processEnabled(final Element enabled) {
970 final String streamId;
971 if (enabled.getAttributeAsBoolean("resume")) {
972 streamId = enabled.getAttribute("id");
973 Log.d(
974 Config.LOGTAG,
975 account.getJid().asBareJid().toString()
976 + ": stream management enabled (resumable)");
977 } else {
978 Log.d(
979 Config.LOGTAG,
980 account.getJid().asBareJid().toString() + ": stream management enabled");
981 streamId = null;
982 }
983 this.streamId = streamId;
984 this.stanzasReceived = 0;
985 this.inSmacksSession = true;
986 final RequestPacket r = new RequestPacket();
987 tagWriter.writeStanzaAsync(r);
988 }
989
990 private void processResumed(final Element resumed) throws StateChangingException {
991 this.inSmacksSession = true;
992 this.isBound = true;
993 this.tagWriter.writeStanzaAsync(new RequestPacket());
994 lastPacketReceived = SystemClock.elapsedRealtime();
995 final Optional<Integer> h = resumed.getOptionalIntAttribute("h");
996 final int serverCount;
997 if (h.isPresent()) {
998 serverCount = h.get();
999 } else {
1000 resetStreamId();
1001 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1002 }
1003 final ArrayList<AbstractAcknowledgeableStanza> failedStanzas = new ArrayList<>();
1004 final boolean acknowledgedMessages;
1005 synchronized (this.mStanzaQueue) {
1006 if (serverCount < stanzasSent) {
1007 Log.d(
1008 Config.LOGTAG,
1009 account.getJid().asBareJid() + ": session resumed with lost packages");
1010 stanzasSent = serverCount;
1011 } else {
1012 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": session resumed");
1013 }
1014 acknowledgedMessages = acknowledgeStanzaUpTo(serverCount);
1015 for (int i = 0; i < this.mStanzaQueue.size(); ++i) {
1016 failedStanzas.add(mStanzaQueue.valueAt(i));
1017 }
1018 mStanzaQueue.clear();
1019 }
1020 if (acknowledgedMessages) {
1021 mXmppConnectionService.updateConversationUi();
1022 }
1023 Log.d(
1024 Config.LOGTAG,
1025 account.getJid().asBareJid() + ": resending " + failedStanzas.size() + " stanzas");
1026 for (final AbstractAcknowledgeableStanza packet : failedStanzas) {
1027 if (packet instanceof MessagePacket) {
1028 MessagePacket message = (MessagePacket) packet;
1029 mXmppConnectionService.markMessage(
1030 account,
1031 message.getTo().asBareJid(),
1032 message.getId(),
1033 Message.STATUS_UNSEND);
1034 }
1035 sendPacket(packet);
1036 }
1037 changeStatusToOnline();
1038 }
1039
1040 private void changeStatusToOnline() {
1041 Log.d(
1042 Config.LOGTAG,
1043 account.getJid().asBareJid() + ": online with resource " + account.getResource());
1044 changeStatus(Account.State.ONLINE);
1045 }
1046
1047 private void processFailed(final Element failed, final boolean sendBindRequest) {
1048 final Optional<Integer> serverCount = failed.getOptionalIntAttribute("h");
1049 if (serverCount.isPresent()) {
1050 Log.d(
1051 Config.LOGTAG,
1052 account.getJid().asBareJid()
1053 + ": resumption failed but server acknowledged stanza #"
1054 + serverCount.get());
1055 final boolean acknowledgedMessages;
1056 synchronized (this.mStanzaQueue) {
1057 acknowledgedMessages = acknowledgeStanzaUpTo(serverCount.get());
1058 }
1059 if (acknowledgedMessages) {
1060 mXmppConnectionService.updateConversationUi();
1061 }
1062 } else {
1063 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resumption failed");
1064 }
1065 resetStreamId();
1066 if (sendBindRequest) {
1067 sendBindRequest();
1068 }
1069 }
1070
1071 private boolean acknowledgeStanzaUpTo(final int serverCount) {
1072 if (serverCount > stanzasSent) {
1073 Log.e(
1074 Config.LOGTAG,
1075 "server acknowledged more stanzas than we sent. serverCount="
1076 + serverCount
1077 + ", ourCount="
1078 + stanzasSent);
1079 }
1080 boolean acknowledgedMessages = false;
1081 for (int i = 0; i < mStanzaQueue.size(); ++i) {
1082 if (serverCount >= mStanzaQueue.keyAt(i)) {
1083 if (Config.EXTENDED_SM_LOGGING) {
1084 Log.d(
1085 Config.LOGTAG,
1086 account.getJid().asBareJid()
1087 + ": server acknowledged stanza #"
1088 + mStanzaQueue.keyAt(i));
1089 }
1090 final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i);
1091 if (stanza instanceof MessagePacket && acknowledgedListener != null) {
1092 final MessagePacket packet = (MessagePacket) stanza;
1093 final String id = packet.getId();
1094 final Jid to = packet.getTo();
1095 if (id != null && to != null) {
1096 acknowledgedMessages |=
1097 acknowledgedListener.onMessageAcknowledged(account, to, id);
1098 }
1099 }
1100 mStanzaQueue.removeAt(i);
1101 i--;
1102 }
1103 }
1104 return acknowledgedMessages;
1105 }
1106
1107 private @NonNull Element processPacket(final Tag currentTag, final int packetType)
1108 throws IOException {
1109 final Element element;
1110 switch (packetType) {
1111 case PACKET_IQ:
1112 element = new IqPacket();
1113 break;
1114 case PACKET_MESSAGE:
1115 element = new MessagePacket();
1116 break;
1117 case PACKET_PRESENCE:
1118 element = new PresencePacket();
1119 break;
1120 default:
1121 throw new AssertionError("Should never encounter invalid type");
1122 }
1123 element.setAttributes(currentTag.getAttributes());
1124 Tag nextTag = tagReader.readTag();
1125 if (nextTag == null) {
1126 throw new IOException("interrupted mid tag");
1127 }
1128 while (!nextTag.isEnd(element.getName())) {
1129 if (!nextTag.isNo()) {
1130 element.addChild(tagReader.readElement(nextTag));
1131 }
1132 nextTag = tagReader.readTag();
1133 if (nextTag == null) {
1134 throw new IOException("interrupted mid tag");
1135 }
1136 }
1137 if (stanzasReceived == Integer.MAX_VALUE) {
1138 resetStreamId();
1139 throw new IOException("time to restart the session. cant handle >2 billion pcks");
1140 }
1141 if (inSmacksSession) {
1142 ++stanzasReceived;
1143 } else if (features.sm()) {
1144 Log.d(
1145 Config.LOGTAG,
1146 account.getJid().asBareJid()
1147 + ": not counting stanza("
1148 + element.getClass().getSimpleName()
1149 + "). Not in smacks session.");
1150 }
1151 lastPacketReceived = SystemClock.elapsedRealtime();
1152 if (Config.BACKGROUND_STANZA_LOGGING && mXmppConnectionService.checkListeners()) {
1153 Log.d(Config.LOGTAG, "[background stanza] " + element);
1154 }
1155 if (element instanceof IqPacket
1156 && (((IqPacket) element).getType() == IqPacket.TYPE.SET)
1157 && element.hasChild("jingle", Namespace.JINGLE)) {
1158 return JinglePacket.upgrade((IqPacket) element);
1159 } else {
1160 return element;
1161 }
1162 }
1163
1164 private void processIq(final Tag currentTag) throws IOException {
1165 final IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
1166 if (!packet.valid()) {
1167 Log.e(
1168 Config.LOGTAG,
1169 "encountered invalid iq from='"
1170 + packet.getFrom()
1171 + "' to='"
1172 + packet.getTo()
1173 + "'");
1174 return;
1175 }
1176 if (packet instanceof JinglePacket) {
1177 if (this.jingleListener != null) {
1178 this.jingleListener.onJinglePacketReceived(account, (JinglePacket) packet);
1179 }
1180 } else {
1181 OnIqPacketReceived callback = null;
1182 synchronized (this.packetCallbacks) {
1183 final Pair<IqPacket, OnIqPacketReceived> packetCallbackDuple =
1184 packetCallbacks.get(packet.getId());
1185 if (packetCallbackDuple != null) {
1186 // Packets to the server should have responses from the server
1187 if (packetCallbackDuple.first.toServer(account)) {
1188 if (packet.fromServer(account)) {
1189 callback = packetCallbackDuple.second;
1190 packetCallbacks.remove(packet.getId());
1191 } else {
1192 Log.e(
1193 Config.LOGTAG,
1194 account.getJid().asBareJid().toString()
1195 + ": ignoring spoofed iq packet");
1196 }
1197 } else {
1198 if (packet.getFrom() != null
1199 && packet.getFrom().equals(packetCallbackDuple.first.getTo())) {
1200 callback = packetCallbackDuple.second;
1201 packetCallbacks.remove(packet.getId());
1202 } else {
1203 Log.e(
1204 Config.LOGTAG,
1205 account.getJid().asBareJid().toString()
1206 + ": ignoring spoofed iq packet");
1207 }
1208 }
1209 } else if (packet.getType() == IqPacket.TYPE.GET
1210 || packet.getType() == IqPacket.TYPE.SET) {
1211 callback = this.unregisteredIqListener;
1212 }
1213 }
1214 if (callback != null) {
1215 try {
1216 callback.onIqPacketReceived(account, packet);
1217 } catch (StateChangingError error) {
1218 throw new StateChangingException(error.state);
1219 }
1220 }
1221 }
1222 }
1223
1224 private void processMessage(final Tag currentTag) throws IOException {
1225 final MessagePacket packet = (MessagePacket) processPacket(currentTag, PACKET_MESSAGE);
1226 if (!packet.valid()) {
1227 Log.e(
1228 Config.LOGTAG,
1229 "encountered invalid message from='"
1230 + packet.getFrom()
1231 + "' to='"
1232 + packet.getTo()
1233 + "'");
1234 return;
1235 }
1236 this.messageListener.onMessagePacketReceived(account, packet);
1237 }
1238
1239 private void processPresence(final Tag currentTag) throws IOException {
1240 PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE);
1241 if (!packet.valid()) {
1242 Log.e(
1243 Config.LOGTAG,
1244 "encountered invalid presence from='"
1245 + packet.getFrom()
1246 + "' to='"
1247 + packet.getTo()
1248 + "'");
1249 return;
1250 }
1251 this.presenceListener.onPresencePacketReceived(account, packet);
1252 }
1253
1254 private void sendStartTLS() throws IOException {
1255 final Tag startTLS = Tag.empty("starttls");
1256 startTLS.setAttribute("xmlns", Namespace.TLS);
1257 tagWriter.writeTag(startTLS);
1258 }
1259
1260 private void switchOverToTls() throws XmlPullParserException, IOException {
1261 tagReader.readTag();
1262 final Socket socket = this.socket;
1263 final SSLSocket sslSocket = upgradeSocketToTls(socket);
1264 this.socket = sslSocket;
1265 this.tagReader.setInputStream(sslSocket.getInputStream());
1266 this.tagWriter.setOutputStream(sslSocket.getOutputStream());
1267 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS connection established");
1268 final boolean quickStart;
1269 try {
1270 quickStart = establishStream(SSLSockets.version(sslSocket));
1271 } catch (final InterruptedException e) {
1272 return;
1273 }
1274 if (quickStart) {
1275 this.quickStartInProgress = true;
1276 }
1277 features.encryptionEnabled = true;
1278 final Tag tag = tagReader.readTag();
1279 if (tag != null && tag.isStart("stream", Namespace.STREAMS)) {
1280 SSLSockets.log(account, sslSocket);
1281 processStream();
1282 } else {
1283 throw new StateChangingException(Account.State.STREAM_OPENING_ERROR);
1284 }
1285 sslSocket.close();
1286 }
1287
1288 private SSLSocket upgradeSocketToTls(final Socket socket) throws IOException {
1289 final SSLSocketFactory sslSocketFactory;
1290 try {
1291 sslSocketFactory = getSSLSocketFactory();
1292 } catch (final NoSuchAlgorithmException | KeyManagementException e) {
1293 throw new StateChangingException(Account.State.TLS_ERROR);
1294 }
1295 final InetAddress address = socket.getInetAddress();
1296 final SSLSocket sslSocket =
1297 (SSLSocket)
1298 sslSocketFactory.createSocket(
1299 socket, address.getHostAddress(), socket.getPort(), true);
1300 SSLSockets.setSecurity(sslSocket);
1301 SSLSockets.setHostname(sslSocket, IDN.toASCII(account.getServer()));
1302 SSLSockets.setApplicationProtocol(sslSocket, "xmpp-client");
1303 final XmppDomainVerifier xmppDomainVerifier = new XmppDomainVerifier();
1304 try {
1305 if (!xmppDomainVerifier.verify(
1306 account.getServer(), this.verifiedHostname, sslSocket.getSession())) {
1307 Log.d(
1308 Config.LOGTAG,
1309 account.getJid().asBareJid()
1310 + ": TLS certificate domain verification failed");
1311 FileBackend.close(sslSocket);
1312 throw new StateChangingException(Account.State.TLS_ERROR_DOMAIN);
1313 }
1314 } catch (final SSLPeerUnverifiedException e) {
1315 FileBackend.close(sslSocket);
1316 throw new StateChangingException(Account.State.TLS_ERROR);
1317 }
1318 return sslSocket;
1319 }
1320
1321 private void processStreamFeatures(final Tag currentTag) throws IOException {
1322 this.streamFeatures = tagReader.readElement(currentTag);
1323 final boolean isSecure = isSecure();
1324 final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER);
1325 if (this.quickStartInProgress) {
1326 if (this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) {
1327 Log.d(
1328 Config.LOGTAG,
1329 account.getJid().asBareJid()
1330 + ": quick start in progress. ignoring features: "
1331 + XmlHelper.printElementNames(this.streamFeatures));
1332 if (SaslMechanism.hashedToken(this.saslMechanism)) {
1333 return;
1334 }
1335 if (isFastTokenAvailable(
1336 this.streamFeatures.findChild("authentication", Namespace.SASL_2))) {
1337 Log.d(
1338 Config.LOGTAG,
1339 account.getJid().asBareJid()
1340 + ": fast token available; resetting quick start");
1341 account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, false);
1342 mXmppConnectionService.databaseBackend.updateAccount(account);
1343 }
1344 return;
1345 }
1346 Log.d(
1347 Config.LOGTAG,
1348 account.getJid().asBareJid()
1349 + ": server lost support for SASL 2. quick start not possible");
1350 this.account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, false);
1351 mXmppConnectionService.databaseBackend.updateAccount(account);
1352 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1353 }
1354 if (this.streamFeatures.hasChild("starttls", Namespace.TLS)
1355 && !features.encryptionEnabled) {
1356 sendStartTLS();
1357 } else if (this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE)
1358 && account.isOptionSet(Account.OPTION_REGISTER)) {
1359 if (isSecure) {
1360 register();
1361 } else {
1362 Log.d(
1363 Config.LOGTAG,
1364 account.getJid().asBareJid()
1365 + ": unable to find STARTTLS for registration process "
1366 + XmlHelper.printElementNames(this.streamFeatures));
1367 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1368 }
1369 } else if (!this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE)
1370 && account.isOptionSet(Account.OPTION_REGISTER)) {
1371 throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED);
1372 } else if (this.streamFeatures.hasChild("authentication", Namespace.SASL_2)
1373 && shouldAuthenticate
1374 && isSecure) {
1375 authenticate(SaslMechanism.Version.SASL_2);
1376 } else if (this.streamFeatures.hasChild("mechanisms", Namespace.SASL)
1377 && shouldAuthenticate
1378 && isSecure) {
1379 authenticate(SaslMechanism.Version.SASL);
1380 } else if (this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT)
1381 && streamId != null
1382 && !inSmacksSession) {
1383 if (Config.EXTENDED_SM_LOGGING) {
1384 Log.d(
1385 Config.LOGTAG,
1386 account.getJid().asBareJid()
1387 + ": resuming after stanza #"
1388 + stanzasReceived);
1389 }
1390 final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived);
1391 this.mSmCatchupMessageCounter.set(0);
1392 this.mWaitingForSmCatchup.set(true);
1393 this.tagWriter.writeStanzaAsync(resume);
1394 } else if (needsBinding) {
1395 if (this.streamFeatures.hasChild("bind", Namespace.BIND) && isSecure) {
1396 sendBindRequest();
1397 } else {
1398 Log.d(
1399 Config.LOGTAG,
1400 account.getJid().asBareJid()
1401 + ": unable to find bind feature "
1402 + XmlHelper.printElementNames(this.streamFeatures));
1403 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1404 }
1405 } else {
1406
1407 Log.d(
1408 Config.LOGTAG,
1409 account.getJid().asBareJid()
1410 + ": received NOP stream features: "
1411 + XmlHelper.printElementNames(this.streamFeatures));
1412 }
1413 }
1414
1415 private void authenticate() throws IOException {
1416 final boolean isSecure = isSecure();
1417 if (isSecure && this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) {
1418 authenticate(SaslMechanism.Version.SASL_2);
1419 } else if (isSecure && this.streamFeatures.hasChild("mechanisms", Namespace.SASL)) {
1420 authenticate(SaslMechanism.Version.SASL);
1421 } else {
1422 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1423 }
1424 }
1425
1426 private boolean isSecure() {
1427 return features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion();
1428 }
1429
1430 private void authenticate(final SaslMechanism.Version version) throws IOException {
1431 final Element authElement;
1432 if (version == SaslMechanism.Version.SASL) {
1433 authElement = this.streamFeatures.findChild("mechanisms", Namespace.SASL);
1434 } else {
1435 authElement = this.streamFeatures.findChild("authentication", Namespace.SASL_2);
1436 }
1437 final Collection<String> mechanisms = SaslMechanism.mechanisms(authElement);
1438 final Element cbElement =
1439 this.streamFeatures.findChild("sasl-channel-binding", Namespace.CHANNEL_BINDING);
1440 final Collection<ChannelBinding> channelBindings = ChannelBinding.of(cbElement);
1441 final SaslMechanism.Factory factory = new SaslMechanism.Factory(account);
1442 final SaslMechanism saslMechanism = factory.of(mechanisms, channelBindings, version, SSLSockets.version(this.socket));
1443 this.saslMechanism = validate(saslMechanism, mechanisms);
1444 final boolean quickStartAvailable;
1445 final String firstMessage = this.saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket));
1446 final boolean usingFast = SaslMechanism.hashedToken(this.saslMechanism);
1447 final Element authenticate;
1448 if (version == SaslMechanism.Version.SASL) {
1449 authenticate = new Element("auth", Namespace.SASL);
1450 if (!Strings.isNullOrEmpty(firstMessage)) {
1451 authenticate.setContent(firstMessage);
1452 }
1453 quickStartAvailable = false;
1454 } else if (version == SaslMechanism.Version.SASL_2) {
1455 final Element inline = authElement.findChild("inline", Namespace.SASL_2);
1456 final boolean sm = inline != null && inline.hasChild("sm", Namespace.STREAM_MANAGEMENT);
1457 final HashedToken.Mechanism hashTokenRequest;
1458 if (usingFast) {
1459 hashTokenRequest = null;
1460 } else {
1461 final Element fast = inline == null ? null : inline.findChild("fast", Namespace.FAST);
1462 final Collection<String> fastMechanisms = SaslMechanism.mechanisms(fast);
1463 hashTokenRequest =
1464 HashedToken.Mechanism.best(fastMechanisms, SSLSockets.version(this.socket));
1465 }
1466 final Collection<String> bindFeatures = Bind2.features(inline);
1467 quickStartAvailable =
1468 sm
1469 && bindFeatures != null
1470 && bindFeatures.containsAll(Bind2.QUICKSTART_FEATURES);
1471 if (bindFeatures != null) {
1472 try {
1473 mXmppConnectionService.restoredFromDatabaseLatch.await();
1474 } catch (final InterruptedException e) {
1475 Log.d(
1476 Config.LOGTAG,
1477 account.getJid().asBareJid()
1478 + ": interrupted while waiting for DB restore during SASL2 bind");
1479 return;
1480 }
1481 }
1482 this.hashTokenRequest = hashTokenRequest;
1483 authenticate = generateAuthenticationRequest(firstMessage, usingFast, hashTokenRequest, bindFeatures, sm);
1484 } else {
1485 throw new AssertionError("Missing implementation for " + version);
1486 }
1487
1488 if (account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, quickStartAvailable)) {
1489 mXmppConnectionService.databaseBackend.updateAccount(account);
1490 }
1491
1492 Log.d(
1493 Config.LOGTAG,
1494 account.getJid().toString()
1495 + ": Authenticating with "
1496 + version
1497 + "/"
1498 + this.saslMechanism.getMechanism());
1499 authenticate.setAttribute("mechanism", this.saslMechanism.getMechanism());
1500 synchronized (this.mStanzaQueue) {
1501 this.stanzasSentBeforeAuthentication = this.stanzasSent;
1502 tagWriter.writeElement(authenticate);
1503 }
1504 }
1505
1506 private static boolean isFastTokenAvailable(final Element authentication) {
1507 final Element inline = authentication == null ? null : authentication.findChild("inline");
1508 return inline != null && inline.hasChild("fast", Namespace.FAST);
1509 }
1510
1511 @NonNull
1512 private SaslMechanism validate(final @Nullable SaslMechanism saslMechanism, Collection<String> mechanisms) throws StateChangingException {
1513 if (saslMechanism == null) {
1514 Log.d(
1515 Config.LOGTAG,
1516 account.getJid().asBareJid()
1517 + ": unable to find supported SASL mechanism in "
1518 + mechanisms);
1519 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1520 }
1521 if (SaslMechanism.hashedToken(saslMechanism)) {
1522 return saslMechanism;
1523 }
1524 final int pinnedMechanism = account.getPinnedMechanismPriority();
1525 if (pinnedMechanism > saslMechanism.getPriority()) {
1526 Log.e(
1527 Config.LOGTAG,
1528 "Auth failed. Authentication mechanism "
1529 + saslMechanism.getMechanism()
1530 + " has lower priority ("
1531 + saslMechanism.getPriority()
1532 + ") than pinned priority ("
1533 + pinnedMechanism
1534 + "). Possible downgrade attack?");
1535 throw new StateChangingException(Account.State.DOWNGRADE_ATTACK);
1536 }
1537 return saslMechanism;
1538 }
1539
1540 private Element generateAuthenticationRequest(final String firstMessage, final boolean usingFast) {
1541 return generateAuthenticationRequest(firstMessage, usingFast, null, Bind2.QUICKSTART_FEATURES, true);
1542 }
1543
1544 private Element generateAuthenticationRequest(
1545 final String firstMessage,
1546 final boolean usingFast,
1547 final HashedToken.Mechanism hashedTokenRequest,
1548 final Collection<String> bind,
1549 final boolean inlineStreamManagement) {
1550 final Element authenticate = new Element("authenticate", Namespace.SASL_2);
1551 if (!Strings.isNullOrEmpty(firstMessage)) {
1552 authenticate.addChild("initial-response").setContent(firstMessage);
1553 }
1554 final Element userAgent = authenticate.addChild("user-agent");
1555 userAgent.setAttribute("id", AccountUtils.publicDeviceId(account));
1556 userAgent
1557 .addChild("software")
1558 .setContent(mXmppConnectionService.getString(R.string.app_name));
1559 if (!PhoneHelper.isEmulator()) {
1560 userAgent
1561 .addChild("device")
1562 .setContent(String.format("%s %s", Build.MANUFACTURER, Build.MODEL));
1563 }
1564 // do not include bind if 'inlinestreamManagment' is missing and we have a streamId
1565 final boolean mayAttemptBind = streamId == null || inlineStreamManagement;
1566 if (bind != null && mayAttemptBind) {
1567 authenticate.addChild(generateBindRequest(bind));
1568 }
1569 if (inlineStreamManagement && streamId != null) {
1570 final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived);
1571 this.mSmCatchupMessageCounter.set(0);
1572 this.mWaitingForSmCatchup.set(true);
1573 authenticate.addChild(resume);
1574 }
1575 if (hashedTokenRequest != null) {
1576 authenticate
1577 .addChild("request-token", Namespace.FAST)
1578 .setAttribute("mechanism", hashedTokenRequest.name());
1579 }
1580 if (usingFast) {
1581 authenticate.addChild("fast", Namespace.FAST);
1582 }
1583 return authenticate;
1584 }
1585
1586 private Element generateBindRequest(final Collection<String> bindFeatures) {
1587 Log.d(Config.LOGTAG, "inline bind features: " + bindFeatures);
1588 final Element bind = new Element("bind", Namespace.BIND2);
1589 bind.addChild("tag").setContent(mXmppConnectionService.getString(R.string.app_name));
1590 if (bindFeatures.contains(Namespace.CARBONS)) {
1591 bind.addChild("enable", Namespace.CARBONS);
1592 }
1593 if (bindFeatures.contains(Namespace.STREAM_MANAGEMENT)) {
1594 bind.addChild(new EnablePacket());
1595 }
1596 return bind;
1597 }
1598
1599 private void register() {
1600 final String preAuth = account.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN);
1601 if (preAuth != null && features.invite()) {
1602 final IqPacket preAuthRequest = new IqPacket(IqPacket.TYPE.SET);
1603 preAuthRequest.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth);
1604 sendUnmodifiedIqPacket(
1605 preAuthRequest,
1606 (account, response) -> {
1607 if (response.getType() == IqPacket.TYPE.RESULT) {
1608 sendRegistryRequest();
1609 } else {
1610 final String error = response.getErrorCondition();
1611 Log.d(
1612 Config.LOGTAG,
1613 account.getJid().asBareJid()
1614 + ": failed to pre auth. "
1615 + error);
1616 throw new StateChangingError(Account.State.REGISTRATION_INVALID_TOKEN);
1617 }
1618 },
1619 true);
1620 } else {
1621 sendRegistryRequest();
1622 }
1623 }
1624
1625 private void sendRegistryRequest() {
1626 final IqPacket register = new IqPacket(IqPacket.TYPE.GET);
1627 register.query(Namespace.REGISTER);
1628 register.setTo(account.getDomain());
1629 sendUnmodifiedIqPacket(
1630 register,
1631 (account, packet) -> {
1632 if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
1633 return;
1634 }
1635 if (packet.getType() == IqPacket.TYPE.ERROR) {
1636 throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1637 }
1638 final Element query = packet.query(Namespace.REGISTER);
1639 if (query.hasChild("username") && (query.hasChild("password"))) {
1640 final IqPacket register1 = new IqPacket(IqPacket.TYPE.SET);
1641 final Element username =
1642 new Element("username").setContent(account.getUsername());
1643 final Element password =
1644 new Element("password").setContent(account.getPassword());
1645 register1.query(Namespace.REGISTER).addChild(username);
1646 register1.query().addChild(password);
1647 register1.setFrom(account.getJid().asBareJid());
1648 sendUnmodifiedIqPacket(register1, registrationResponseListener, true);
1649 } else if (query.hasChild("x", Namespace.DATA)) {
1650 final Data data = Data.parse(query.findChild("x", Namespace.DATA));
1651 final Element blob = query.findChild("data", "urn:xmpp:bob");
1652 final String id = packet.getId();
1653 InputStream is;
1654 if (blob != null) {
1655 try {
1656 final String base64Blob = blob.getContent();
1657 final byte[] strBlob = Base64.decode(base64Blob, Base64.DEFAULT);
1658 is = new ByteArrayInputStream(strBlob);
1659 } catch (Exception e) {
1660 is = null;
1661 }
1662 } else {
1663 final boolean useTor =
1664 mXmppConnectionService.useTorToConnect() || account.isOnion();
1665 try {
1666 final String url = data.getValue("url");
1667 final String fallbackUrl = data.getValue("captcha-fallback-url");
1668 if (url != null) {
1669 is = HttpConnectionManager.open(url, useTor);
1670 } else if (fallbackUrl != null) {
1671 is = HttpConnectionManager.open(fallbackUrl, useTor);
1672 } else {
1673 is = null;
1674 }
1675 } catch (final IOException e) {
1676 Log.d(
1677 Config.LOGTAG,
1678 account.getJid().asBareJid() + ": unable to fetch captcha",
1679 e);
1680 is = null;
1681 }
1682 }
1683
1684 if (is != null) {
1685 Bitmap captcha = BitmapFactory.decodeStream(is);
1686 try {
1687 if (mXmppConnectionService.displayCaptchaRequest(
1688 account, id, data, captcha)) {
1689 return;
1690 }
1691 } catch (Exception e) {
1692 throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1693 }
1694 }
1695 throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1696 } else if (query.hasChild("instructions")
1697 || query.hasChild("x", Namespace.OOB)) {
1698 final String instructions = query.findChildContent("instructions");
1699 final Element oob = query.findChild("x", Namespace.OOB);
1700 final String url = oob == null ? null : oob.findChildContent("url");
1701 if (url != null) {
1702 setAccountCreationFailed(url);
1703 } else if (instructions != null) {
1704 final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(instructions);
1705 if (matcher.find()) {
1706 setAccountCreationFailed(
1707 instructions.substring(matcher.start(), matcher.end()));
1708 }
1709 }
1710 throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1711 }
1712 },
1713 true);
1714 }
1715
1716 private void setAccountCreationFailed(final String url) {
1717 final HttpUrl httpUrl = url == null ? null : HttpUrl.parse(url);
1718 if (httpUrl != null && httpUrl.isHttps()) {
1719 this.redirectionUrl = httpUrl;
1720 throw new StateChangingError(Account.State.REGISTRATION_WEB);
1721 }
1722 throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1723 }
1724
1725 public HttpUrl getRedirectionUrl() {
1726 return this.redirectionUrl;
1727 }
1728
1729 public void resetEverything() {
1730 resetAttemptCount(true);
1731 resetStreamId();
1732 clearIqCallbacks();
1733 this.stanzasSent = 0;
1734 mStanzaQueue.clear();
1735 this.redirectionUrl = null;
1736 synchronized (this.disco) {
1737 disco.clear();
1738 }
1739 synchronized (this.commands) {
1740 this.commands.clear();
1741 }
1742 this.saslMechanism = null;
1743 }
1744
1745 private void sendBindRequest() {
1746 try {
1747 mXmppConnectionService.restoredFromDatabaseLatch.await();
1748 } catch (InterruptedException e) {
1749 Log.d(
1750 Config.LOGTAG,
1751 account.getJid().asBareJid()
1752 + ": interrupted while waiting for DB restore during bind");
1753 return;
1754 }
1755 clearIqCallbacks();
1756 if (account.getJid().isBareJid()) {
1757 account.setResource(this.createNewResource());
1758 } else {
1759 fixResource(mXmppConnectionService, account);
1760 }
1761 final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
1762 final String resource =
1763 Config.USE_RANDOM_RESOURCE_ON_EVERY_BIND ? nextRandomId() : account.getResource();
1764 iq.addChild("bind", Namespace.BIND).addChild("resource").setContent(resource);
1765 this.sendUnmodifiedIqPacket(
1766 iq,
1767 (account, packet) -> {
1768 if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
1769 return;
1770 }
1771 final Element bind = packet.findChild("bind");
1772 if (bind != null && packet.getType() == IqPacket.TYPE.RESULT) {
1773 isBound = true;
1774 final Element jid = bind.findChild("jid");
1775 if (jid != null && jid.getContent() != null) {
1776 try {
1777 Jid assignedJid = Jid.ofEscaped(jid.getContent());
1778 if (!account.getJid().getDomain().equals(assignedJid.getDomain())) {
1779 Log.d(
1780 Config.LOGTAG,
1781 account.getJid().asBareJid()
1782 + ": server tried to re-assign domain to "
1783 + assignedJid.getDomain());
1784 throw new StateChangingError(Account.State.BIND_FAILURE);
1785 }
1786 if (account.setJid(assignedJid)) {
1787 Log.d(
1788 Config.LOGTAG,
1789 account.getJid().asBareJid()
1790 + ": jid changed during bind. updating database");
1791 mXmppConnectionService.databaseBackend.updateAccount(account);
1792 }
1793 if (streamFeatures.hasChild("session")
1794 && !streamFeatures
1795 .findChild("session")
1796 .hasChild("optional")) {
1797 sendStartSession();
1798 } else {
1799 final boolean waitForDisco = enableStreamManagement();
1800 sendPostBindInitialization(waitForDisco, false);
1801 }
1802 return;
1803 } catch (final IllegalArgumentException e) {
1804 Log.d(
1805 Config.LOGTAG,
1806 account.getJid().asBareJid()
1807 + ": server reported invalid jid ("
1808 + jid.getContent()
1809 + ") on bind");
1810 }
1811 } else {
1812 Log.d(
1813 Config.LOGTAG,
1814 account.getJid()
1815 + ": disconnecting because of bind failure. (no jid)");
1816 }
1817 } else {
1818 Log.d(
1819 Config.LOGTAG,
1820 account.getJid()
1821 + ": disconnecting because of bind failure ("
1822 + packet);
1823 }
1824 final Element error = packet.findChild("error");
1825 if (packet.getType() == IqPacket.TYPE.ERROR
1826 && error != null
1827 && error.hasChild("conflict")) {
1828 account.setResource(createNewResource());
1829 }
1830 throw new StateChangingError(Account.State.BIND_FAILURE);
1831 },
1832 true);
1833 }
1834
1835 private void clearIqCallbacks() {
1836 final IqPacket failurePacket = new IqPacket(IqPacket.TYPE.TIMEOUT);
1837 final ArrayList<OnIqPacketReceived> callbacks = new ArrayList<>();
1838 synchronized (this.packetCallbacks) {
1839 if (this.packetCallbacks.size() == 0) {
1840 return;
1841 }
1842 Log.d(
1843 Config.LOGTAG,
1844 account.getJid().asBareJid()
1845 + ": clearing "
1846 + this.packetCallbacks.size()
1847 + " iq callbacks");
1848 final Iterator<Pair<IqPacket, OnIqPacketReceived>> iterator =
1849 this.packetCallbacks.values().iterator();
1850 while (iterator.hasNext()) {
1851 Pair<IqPacket, OnIqPacketReceived> entry = iterator.next();
1852 callbacks.add(entry.second);
1853 iterator.remove();
1854 }
1855 }
1856 for (OnIqPacketReceived callback : callbacks) {
1857 try {
1858 callback.onIqPacketReceived(account, failurePacket);
1859 } catch (StateChangingError error) {
1860 Log.d(
1861 Config.LOGTAG,
1862 account.getJid().asBareJid()
1863 + ": caught StateChangingError("
1864 + error.state.toString()
1865 + ") while clearing callbacks");
1866 // ignore
1867 }
1868 }
1869 Log.d(
1870 Config.LOGTAG,
1871 account.getJid().asBareJid()
1872 + ": done clearing iq callbacks. "
1873 + this.packetCallbacks.size()
1874 + " left");
1875 }
1876
1877 public void sendDiscoTimeout() {
1878 if (mWaitForDisco.compareAndSet(true, false)) {
1879 Log.d(
1880 Config.LOGTAG,
1881 account.getJid().asBareJid() + ": finalizing bind after disco timeout");
1882 finalizeBind();
1883 }
1884 }
1885
1886 private void sendStartSession() {
1887 Log.d(
1888 Config.LOGTAG,
1889 account.getJid().asBareJid() + ": sending legacy session to outdated server");
1890 final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET);
1891 startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session");
1892 this.sendUnmodifiedIqPacket(
1893 startSession,
1894 (account, packet) -> {
1895 if (packet.getType() == IqPacket.TYPE.RESULT) {
1896 final boolean waitForDisco = enableStreamManagement();
1897 sendPostBindInitialization(waitForDisco, false);
1898 } else if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
1899 throw new StateChangingError(Account.State.SESSION_FAILURE);
1900 }
1901 },
1902 true);
1903 }
1904
1905 private boolean enableStreamManagement() {
1906 final boolean streamManagement =
1907 this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT);
1908 if (streamManagement) {
1909 synchronized (this.mStanzaQueue) {
1910 final EnablePacket enable = new EnablePacket();
1911 tagWriter.writeStanzaAsync(enable);
1912 stanzasSent = 0;
1913 mStanzaQueue.clear();
1914 }
1915 return true;
1916 } else {
1917 return false;
1918 }
1919 }
1920
1921 private void sendPostBindInitialization(
1922 final boolean waitForDisco, final boolean carbonsEnabled) {
1923 features.carbonsEnabled = carbonsEnabled;
1924 features.blockListRequested = false;
1925 synchronized (this.disco) {
1926 this.disco.clear();
1927 }
1928 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery");
1929 mPendingServiceDiscoveries.set(0);
1930 mWaitForDisco.set(waitForDisco);
1931 lastDiscoStarted = SystemClock.elapsedRealtime();
1932 mXmppConnectionService.scheduleWakeUpCall(
1933 Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
1934 final Element caps = streamFeatures.findChild("c");
1935 final String hash = caps == null ? null : caps.getAttribute("hash");
1936 final String ver = caps == null ? null : caps.getAttribute("ver");
1937 ServiceDiscoveryResult discoveryResult = null;
1938 if (hash != null && ver != null) {
1939 discoveryResult =
1940 mXmppConnectionService.getCachedServiceDiscoveryResult(new Pair<>(hash, ver));
1941 }
1942 final boolean requestDiscoItemsFirst =
1943 !account.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY);
1944 if (requestDiscoItemsFirst) {
1945 sendServiceDiscoveryItems(account.getDomain());
1946 }
1947 if (discoveryResult == null) {
1948 sendServiceDiscoveryInfo(account.getDomain());
1949 } else {
1950 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server caps came from cache");
1951 disco.put(account.getDomain(), discoveryResult);
1952 }
1953 discoverMamPreferences();
1954 sendServiceDiscoveryInfo(account.getJid().asBareJid());
1955 if (!requestDiscoItemsFirst) {
1956 sendServiceDiscoveryItems(account.getDomain());
1957 }
1958
1959 if (!mWaitForDisco.get()) {
1960 finalizeBind();
1961 }
1962 this.lastSessionStarted = SystemClock.elapsedRealtime();
1963 }
1964
1965 private void sendServiceDiscoveryInfo(final Jid jid) {
1966 mPendingServiceDiscoveries.incrementAndGet();
1967 final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
1968 iq.setTo(jid);
1969 iq.query("http://jabber.org/protocol/disco#info");
1970 this.sendIqPacket(
1971 iq,
1972 (account, packet) -> {
1973 if (packet.getType() == IqPacket.TYPE.RESULT) {
1974 boolean advancedStreamFeaturesLoaded;
1975 synchronized (XmppConnection.this.disco) {
1976 ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet);
1977 if (jid.equals(account.getDomain())) {
1978 mXmppConnectionService.databaseBackend.insertDiscoveryResult(
1979 result);
1980 }
1981 disco.put(jid, result);
1982 advancedStreamFeaturesLoaded =
1983 disco.containsKey(account.getDomain())
1984 && disco.containsKey(account.getJid().asBareJid());
1985 }
1986 if (advancedStreamFeaturesLoaded
1987 && (jid.equals(account.getDomain())
1988 || jid.equals(account.getJid().asBareJid()))) {
1989 enableAdvancedStreamFeatures();
1990 }
1991 } else if (packet.getType() == IqPacket.TYPE.ERROR) {
1992 Log.d(
1993 Config.LOGTAG,
1994 account.getJid().asBareJid()
1995 + ": could not query disco info for "
1996 + jid.toString());
1997 final boolean serverOrAccount =
1998 jid.equals(account.getDomain())
1999 || jid.equals(account.getJid().asBareJid());
2000 final boolean advancedStreamFeaturesLoaded;
2001 if (serverOrAccount) {
2002 synchronized (XmppConnection.this.disco) {
2003 disco.put(jid, ServiceDiscoveryResult.empty());
2004 advancedStreamFeaturesLoaded =
2005 disco.containsKey(account.getDomain())
2006 && disco.containsKey(account.getJid().asBareJid());
2007 }
2008 } else {
2009 advancedStreamFeaturesLoaded = false;
2010 }
2011 if (advancedStreamFeaturesLoaded) {
2012 enableAdvancedStreamFeatures();
2013 }
2014 }
2015 if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
2016 if (mPendingServiceDiscoveries.decrementAndGet() == 0
2017 && mWaitForDisco.compareAndSet(true, false)) {
2018 finalizeBind();
2019 }
2020 }
2021 });
2022 }
2023
2024 private void discoverMamPreferences() {
2025 IqPacket request = new IqPacket(IqPacket.TYPE.GET);
2026 request.addChild("prefs", MessageArchiveService.Version.MAM_2.namespace);
2027 sendIqPacket(
2028 request,
2029 (account, response) -> {
2030 if (response.getType() == IqPacket.TYPE.RESULT) {
2031 Element prefs =
2032 response.findChild(
2033 "prefs", MessageArchiveService.Version.MAM_2.namespace);
2034 isMamPreferenceAlways =
2035 "always"
2036 .equals(
2037 prefs == null
2038 ? null
2039 : prefs.getAttribute("default"));
2040 }
2041 });
2042 }
2043
2044 private void discoverCommands() {
2045 final IqPacket request = new IqPacket(IqPacket.TYPE.GET);
2046 request.setTo(account.getDomain());
2047 request.addChild("query", Namespace.DISCO_ITEMS).setAttribute("node", Namespace.COMMANDS);
2048 sendIqPacket(
2049 request,
2050 (account, response) -> {
2051 if (response.getType() == IqPacket.TYPE.RESULT) {
2052 final Element query = response.findChild("query", Namespace.DISCO_ITEMS);
2053 if (query == null) {
2054 return;
2055 }
2056 final HashMap<String, Jid> commands = new HashMap<>();
2057 for (final Element child : query.getChildren()) {
2058 if ("item".equals(child.getName())) {
2059 final String node = child.getAttribute("node");
2060 final Jid jid = child.getAttributeAsJid("jid");
2061 if (node != null && jid != null) {
2062 commands.put(node, jid);
2063 }
2064 }
2065 }
2066 synchronized (this.commands) {
2067 this.commands.clear();
2068 this.commands.putAll(commands);
2069 }
2070 }
2071 });
2072 }
2073
2074 public boolean isMamPreferenceAlways() {
2075 return isMamPreferenceAlways;
2076 }
2077
2078 private void finalizeBind() {
2079 if (bindListener != null) {
2080 bindListener.onBind(account);
2081 }
2082 changeStatusToOnline();
2083 }
2084
2085 private void enableAdvancedStreamFeatures() {
2086 if (getFeatures().blocking() && !features.blockListRequested) {
2087 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Requesting block list");
2088 this.sendIqPacket(
2089 getIqGenerator().generateGetBlockList(), mXmppConnectionService.getIqParser());
2090 }
2091 for (final OnAdvancedStreamFeaturesLoaded listener :
2092 advancedStreamFeaturesLoadedListeners) {
2093 listener.onAdvancedStreamFeaturesAvailable(account);
2094 }
2095 if (getFeatures().carbons() && !features.carbonsEnabled) {
2096 sendEnableCarbons();
2097 }
2098 if (getFeatures().commands()) {
2099 discoverCommands();
2100 }
2101 }
2102
2103 private void sendServiceDiscoveryItems(final Jid server) {
2104 mPendingServiceDiscoveries.incrementAndGet();
2105 final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
2106 iq.setTo(server.getDomain());
2107 iq.query("http://jabber.org/protocol/disco#items");
2108 this.sendIqPacket(
2109 iq,
2110 (account, packet) -> {
2111 if (packet.getType() == IqPacket.TYPE.RESULT) {
2112 final HashSet<Jid> items = new HashSet<>();
2113 final List<Element> elements = packet.query().getChildren();
2114 for (final Element element : elements) {
2115 if (element.getName().equals("item")) {
2116 final Jid jid =
2117 InvalidJid.getNullForInvalid(
2118 element.getAttributeAsJid("jid"));
2119 if (jid != null && !jid.equals(account.getDomain())) {
2120 items.add(jid);
2121 }
2122 }
2123 }
2124 for (Jid jid : items) {
2125 sendServiceDiscoveryInfo(jid);
2126 }
2127 } else {
2128 Log.d(
2129 Config.LOGTAG,
2130 account.getJid().asBareJid()
2131 + ": could not query disco items of "
2132 + server);
2133 }
2134 if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
2135 if (mPendingServiceDiscoveries.decrementAndGet() == 0
2136 && mWaitForDisco.compareAndSet(true, false)) {
2137 finalizeBind();
2138 }
2139 }
2140 });
2141 }
2142
2143 private void sendEnableCarbons() {
2144 final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
2145 iq.addChild("enable", Namespace.CARBONS);
2146 this.sendIqPacket(
2147 iq,
2148 (account, packet) -> {
2149 if (packet.getType() == IqPacket.TYPE.RESULT) {
2150 Log.d(
2151 Config.LOGTAG,
2152 account.getJid().asBareJid() + ": successfully enabled carbons");
2153 features.carbonsEnabled = true;
2154 } else {
2155 Log.d(
2156 Config.LOGTAG,
2157 account.getJid().asBareJid()
2158 + ": could not enable carbons "
2159 + packet);
2160 }
2161 });
2162 }
2163
2164 private void processStreamError(final Tag currentTag) throws IOException {
2165 final Element streamError = tagReader.readElement(currentTag);
2166 if (streamError == null) {
2167 return;
2168 }
2169 if (streamError.hasChild("conflict")) {
2170 account.setResource(createNewResource());
2171 Log.d(
2172 Config.LOGTAG,
2173 account.getJid().asBareJid()
2174 + ": switching resource due to conflict ("
2175 + account.getResource()
2176 + ")");
2177 throw new IOException();
2178 } else if (streamError.hasChild("host-unknown")) {
2179 throw new StateChangingException(Account.State.HOST_UNKNOWN);
2180 } else if (streamError.hasChild("policy-violation")) {
2181 this.lastConnect = SystemClock.elapsedRealtime();
2182 final String text = streamError.findChildContent("text");
2183 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": policy violation. " + text);
2184 failPendingMessages(text);
2185 throw new StateChangingException(Account.State.POLICY_VIOLATION);
2186 } else if (streamError.hasChild("see-other-host")) {
2187 final String seeOtherHost = streamError.findChildContent("see-other-host");
2188 final Resolver.Result currentResolverResult = this.currentResolverResult;
2189 if (Strings.isNullOrEmpty(seeOtherHost) || currentResolverResult == null) {
2190 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError);
2191 throw new StateChangingException(Account.State.STREAM_ERROR);
2192 }
2193 Log.d(Config.LOGTAG,account.getJid().asBareJid()+": see other host: "+seeOtherHost+" "+currentResolverResult);
2194 final Resolver.Result seeOtherResult = currentResolverResult.seeOtherHost(seeOtherHost);
2195 if (seeOtherResult != null) {
2196 this.seeOtherHostResolverResult = seeOtherResult;
2197 throw new StateChangingException(Account.State.SEE_OTHER_HOST);
2198 } else {
2199 throw new StateChangingException(Account.State.STREAM_ERROR);
2200 }
2201 } else {
2202 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError);
2203 throw new StateChangingException(Account.State.STREAM_ERROR);
2204 }
2205 }
2206
2207 private void failPendingMessages(final String error) {
2208 synchronized (this.mStanzaQueue) {
2209 for (int i = 0; i < mStanzaQueue.size(); ++i) {
2210 final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i);
2211 if (stanza instanceof MessagePacket) {
2212 final MessagePacket packet = (MessagePacket) stanza;
2213 final String id = packet.getId();
2214 final Jid to = packet.getTo();
2215 mXmppConnectionService.markMessage(
2216 account, to.asBareJid(), id, Message.STATUS_SEND_FAILED, error);
2217 }
2218 }
2219 }
2220 }
2221
2222 private boolean establishStream(final SSLSockets.Version sslVersion)
2223 throws IOException, InterruptedException {
2224 final boolean secureConnection = sslVersion != SSLSockets.Version.NONE;
2225 final SaslMechanism quickStartMechanism;
2226 if (secureConnection) {
2227 quickStartMechanism = SaslMechanism.ensureAvailable(account.getQuickStartMechanism(), sslVersion);
2228 } else {
2229 quickStartMechanism = null;
2230 }
2231 if (secureConnection
2232 && Config.QUICKSTART_ENABLED
2233 && quickStartMechanism != null
2234 && account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) {
2235 mXmppConnectionService.restoredFromDatabaseLatch.await();
2236 this.saslMechanism = quickStartMechanism;
2237 final boolean usingFast = quickStartMechanism instanceof HashedToken;
2238 final Element authenticate =
2239 generateAuthenticationRequest(quickStartMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)), usingFast);
2240 authenticate.setAttribute("mechanism", quickStartMechanism.getMechanism());
2241 sendStartStream(true, false);
2242 synchronized (this.mStanzaQueue) {
2243 this.stanzasSentBeforeAuthentication = this.stanzasSent;
2244 tagWriter.writeElement(authenticate);
2245 }
2246 Log.d(
2247 Config.LOGTAG,
2248 account.getJid().toString()
2249 + ": quick start with "
2250 + quickStartMechanism.getMechanism());
2251 return true;
2252 } else {
2253 sendStartStream(secureConnection, true);
2254 return false;
2255 }
2256 }
2257
2258 private void sendStartStream(final boolean from, final boolean flush) throws IOException {
2259 final Tag stream = Tag.start("stream:stream");
2260 stream.setAttribute("to", account.getServer());
2261 if (from) {
2262 stream.setAttribute("from", account.getJid().asBareJid().toEscapedString());
2263 }
2264 stream.setAttribute("version", "1.0");
2265 stream.setAttribute("xml:lang", LocalizedContent.STREAM_LANGUAGE);
2266 stream.setAttribute("xmlns", "jabber:client");
2267 stream.setAttribute("xmlns:stream", Namespace.STREAMS);
2268 tagWriter.writeTag(stream, flush);
2269 }
2270
2271 private String createNewResource() {
2272 return mXmppConnectionService.getString(R.string.app_name) + '.' + nextRandomId(true);
2273 }
2274
2275 private String nextRandomId() {
2276 return nextRandomId(false);
2277 }
2278
2279 private String nextRandomId(final boolean s) {
2280 return CryptoHelper.random(s ? 3 : 9);
2281 }
2282
2283 public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) {
2284 packet.setFrom(account.getJid());
2285 return this.sendUnmodifiedIqPacket(packet, callback, false);
2286 }
2287
2288 public synchronized String sendUnmodifiedIqPacket(
2289 final IqPacket packet, final OnIqPacketReceived callback, boolean force) {
2290 if (packet.getId() == null) {
2291 packet.setAttribute("id", nextRandomId());
2292 }
2293 if (callback != null) {
2294 synchronized (this.packetCallbacks) {
2295 packetCallbacks.put(packet.getId(), new Pair<>(packet, callback));
2296 }
2297 }
2298 this.sendPacket(packet, force);
2299 return packet.getId();
2300 }
2301
2302 public void sendMessagePacket(final MessagePacket packet) {
2303 this.sendPacket(packet);
2304 }
2305
2306 public void sendPresencePacket(final PresencePacket packet) {
2307 this.sendPacket(packet);
2308 }
2309
2310 private synchronized void sendPacket(final AbstractStanza packet) {
2311 sendPacket(packet, false);
2312 }
2313
2314 private synchronized void sendPacket(final AbstractStanza packet, final boolean force) {
2315 if (stanzasSent == Integer.MAX_VALUE) {
2316 resetStreamId();
2317 disconnect(true);
2318 return;
2319 }
2320 synchronized (this.mStanzaQueue) {
2321 if (force || isBound) {
2322 tagWriter.writeStanzaAsync(packet);
2323 } else {
2324 Log.d(
2325 Config.LOGTAG,
2326 account.getJid().asBareJid()
2327 + " do not write stanza to unbound stream "
2328 + packet.toString());
2329 }
2330 if (packet instanceof AbstractAcknowledgeableStanza) {
2331 AbstractAcknowledgeableStanza stanza = (AbstractAcknowledgeableStanza) packet;
2332
2333 if (this.mStanzaQueue.size() != 0) {
2334 int currentHighestKey = this.mStanzaQueue.keyAt(this.mStanzaQueue.size() - 1);
2335 if (currentHighestKey != stanzasSent) {
2336 throw new AssertionError("Stanza count messed up");
2337 }
2338 }
2339
2340 ++stanzasSent;
2341 if (Config.EXTENDED_SM_LOGGING) {
2342 Log.d(Config.LOGTAG, account.getJid().asBareJid()+": counting outbound "+packet.getName()+" as #" + stanzasSent);
2343 }
2344 this.mStanzaQueue.append(stanzasSent, stanza);
2345 if (stanza instanceof MessagePacket && stanza.getId() != null && inSmacksSession) {
2346 if (Config.EXTENDED_SM_LOGGING) {
2347 Log.d(
2348 Config.LOGTAG,
2349 account.getJid().asBareJid()
2350 + ": requesting ack for message stanza #"
2351 + stanzasSent);
2352 }
2353 tagWriter.writeStanzaAsync(new RequestPacket());
2354 }
2355 }
2356 }
2357 }
2358
2359 public void sendPing() {
2360 if (!r()) {
2361 final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
2362 iq.setFrom(account.getJid());
2363 iq.addChild("ping", Namespace.PING);
2364 this.sendIqPacket(iq, null);
2365 }
2366 this.lastPingSent = SystemClock.elapsedRealtime();
2367 }
2368
2369 public void setOnMessagePacketReceivedListener(final OnMessagePacketReceived listener) {
2370 this.messageListener = listener;
2371 }
2372
2373 public void setOnUnregisteredIqPacketReceivedListener(final OnIqPacketReceived listener) {
2374 this.unregisteredIqListener = listener;
2375 }
2376
2377 public void setOnPresencePacketReceivedListener(final OnPresencePacketReceived listener) {
2378 this.presenceListener = listener;
2379 }
2380
2381 public void setOnJinglePacketReceivedListener(final OnJinglePacketReceived listener) {
2382 this.jingleListener = listener;
2383 }
2384
2385 public void setOnStatusChangedListener(final OnStatusChanged listener) {
2386 this.statusListener = listener;
2387 }
2388
2389 public void setOnBindListener(final OnBindListener listener) {
2390 this.bindListener = listener;
2391 }
2392
2393 public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) {
2394 this.acknowledgedListener = listener;
2395 }
2396
2397 public void addOnAdvancedStreamFeaturesAvailableListener(
2398 final OnAdvancedStreamFeaturesLoaded listener) {
2399 this.advancedStreamFeaturesLoadedListeners.add(listener);
2400 }
2401
2402 private void forceCloseSocket() {
2403 FileBackend.close(this.socket);
2404 FileBackend.close(this.tagReader);
2405 }
2406
2407 public void interrupt() {
2408 if (this.mThread != null) {
2409 this.mThread.interrupt();
2410 }
2411 }
2412
2413 public void disconnect(final boolean force) {
2414 interrupt();
2415 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": disconnecting force=" + force);
2416 if (force) {
2417 forceCloseSocket();
2418 } else {
2419 final TagWriter currentTagWriter = this.tagWriter;
2420 if (currentTagWriter.isActive()) {
2421 currentTagWriter.finish();
2422 final Socket currentSocket = this.socket;
2423 final CountDownLatch streamCountDownLatch = this.mStreamCountDownLatch;
2424 try {
2425 currentTagWriter.await(1, TimeUnit.SECONDS);
2426 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": closing stream");
2427 currentTagWriter.writeTag(Tag.end("stream:stream"));
2428 if (streamCountDownLatch != null) {
2429 if (streamCountDownLatch.await(1, TimeUnit.SECONDS)) {
2430 Log.d(
2431 Config.LOGTAG,
2432 account.getJid().asBareJid() + ": remote ended stream");
2433 } else {
2434 Log.d(
2435 Config.LOGTAG,
2436 account.getJid().asBareJid()
2437 + ": remote has not closed socket. force closing");
2438 }
2439 }
2440 } catch (InterruptedException e) {
2441 Log.d(
2442 Config.LOGTAG,
2443 account.getJid().asBareJid()
2444 + ": interrupted while gracefully closing stream");
2445 } catch (final IOException e) {
2446 Log.d(
2447 Config.LOGTAG,
2448 account.getJid().asBareJid()
2449 + ": io exception during disconnect ("
2450 + e.getMessage()
2451 + ")");
2452 } finally {
2453 FileBackend.close(currentSocket);
2454 }
2455 } else {
2456 forceCloseSocket();
2457 }
2458 }
2459 }
2460
2461 private void resetStreamId() {
2462 this.streamId = null;
2463 this.boundStreamFeatures = null;
2464 }
2465
2466 private List<Entry<Jid, ServiceDiscoveryResult>> findDiscoItemsByFeature(final String feature) {
2467 synchronized (this.disco) {
2468 final List<Entry<Jid, ServiceDiscoveryResult>> items = new ArrayList<>();
2469 for (final Entry<Jid, ServiceDiscoveryResult> cursor : this.disco.entrySet()) {
2470 if (cursor.getValue().getFeatures().contains(feature)) {
2471 items.add(cursor);
2472 }
2473 }
2474 return items;
2475 }
2476 }
2477
2478 public Jid findDiscoItemByFeature(final String feature) {
2479 final List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(feature);
2480 if (items.size() >= 1) {
2481 return items.get(0).getKey();
2482 }
2483 return null;
2484 }
2485
2486 public boolean r() {
2487 if (getFeatures().sm()) {
2488 this.tagWriter.writeStanzaAsync(new RequestPacket());
2489 return true;
2490 } else {
2491 return false;
2492 }
2493 }
2494
2495 public List<String> getMucServersWithholdAccount() {
2496 final List<String> servers = getMucServers();
2497 servers.remove(account.getDomain().toEscapedString());
2498 return servers;
2499 }
2500
2501 public List<String> getMucServers() {
2502 List<String> servers = new ArrayList<>();
2503 synchronized (this.disco) {
2504 for (final Entry<Jid, ServiceDiscoveryResult> cursor : disco.entrySet()) {
2505 final ServiceDiscoveryResult value = cursor.getValue();
2506 if (value.getFeatures().contains("http://jabber.org/protocol/muc")
2507 && value.hasIdentity("conference", "text")
2508 && !value.getFeatures().contains("jabber:iq:gateway")
2509 && !value.hasIdentity("conference", "irc")) {
2510 servers.add(cursor.getKey().toString());
2511 }
2512 }
2513 }
2514 return servers;
2515 }
2516
2517 public String getMucServer() {
2518 List<String> servers = getMucServers();
2519 return servers.size() > 0 ? servers.get(0) : null;
2520 }
2521
2522 public int getTimeToNextAttempt(final boolean aggressive) {
2523 final int interval;
2524 if (aggressive) {
2525 interval = Math.min((int) (3 * Math.pow(1.3,attempt)), 60);
2526 } else {
2527 final int additionalTime =
2528 account.getLastErrorStatus() == Account.State.POLICY_VIOLATION ? 3 : 0;
2529 interval = Math.min((int) (25 * Math.pow(1.3, (additionalTime + attempt))), 300);
2530 }
2531 final int secondsSinceLast =
2532 (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
2533 return interval - secondsSinceLast;
2534 }
2535
2536 public int getAttempt() {
2537 return this.attempt;
2538 }
2539
2540 public Features getFeatures() {
2541 return this.features;
2542 }
2543
2544 public long getLastSessionEstablished() {
2545 final long diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
2546 return System.currentTimeMillis() - diff;
2547 }
2548
2549 public long getLastConnect() {
2550 return this.lastConnect;
2551 }
2552
2553 public long getLastPingSent() {
2554 return this.lastPingSent;
2555 }
2556
2557 public long getLastDiscoStarted() {
2558 return this.lastDiscoStarted;
2559 }
2560
2561 public long getLastPacketReceived() {
2562 return this.lastPacketReceived;
2563 }
2564
2565 public void sendActive() {
2566 this.sendPacket(new ActivePacket());
2567 }
2568
2569 public void sendInactive() {
2570 this.sendPacket(new InactivePacket());
2571 }
2572
2573 public void resetAttemptCount(boolean resetConnectTime) {
2574 this.attempt = 0;
2575 if (resetConnectTime) {
2576 this.lastConnect = 0;
2577 }
2578 }
2579
2580 public void setInteractive(boolean interactive) {
2581 this.mInteractive = interactive;
2582 }
2583
2584 private IqGenerator getIqGenerator() {
2585 return mXmppConnectionService.getIqGenerator();
2586 }
2587
2588 private class MyKeyManager implements X509KeyManager {
2589 @Override
2590 public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
2591 return account.getPrivateKeyAlias();
2592 }
2593
2594 @Override
2595 public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
2596 return null;
2597 }
2598
2599 @Override
2600 public X509Certificate[] getCertificateChain(String alias) {
2601 Log.d(Config.LOGTAG, "getting certificate chain");
2602 try {
2603 return KeyChain.getCertificateChain(mXmppConnectionService, alias);
2604 } catch (final Exception e) {
2605 Log.d(Config.LOGTAG, "could not get certificate chain", e);
2606 return new X509Certificate[0];
2607 }
2608 }
2609
2610 @Override
2611 public String[] getClientAliases(String s, Principal[] principals) {
2612 final String alias = account.getPrivateKeyAlias();
2613 return alias != null ? new String[] {alias} : new String[0];
2614 }
2615
2616 @Override
2617 public String[] getServerAliases(String s, Principal[] principals) {
2618 return new String[0];
2619 }
2620
2621 @Override
2622 public PrivateKey getPrivateKey(String alias) {
2623 try {
2624 return KeyChain.getPrivateKey(mXmppConnectionService, alias);
2625 } catch (Exception e) {
2626 return null;
2627 }
2628 }
2629 }
2630
2631 private static class StateChangingError extends Error {
2632 private final Account.State state;
2633
2634 public StateChangingError(Account.State state) {
2635 this.state = state;
2636 }
2637 }
2638
2639 private static class StateChangingException extends IOException {
2640 private final Account.State state;
2641
2642 public StateChangingException(Account.State state) {
2643 this.state = state;
2644 }
2645 }
2646
2647 public class Features {
2648 XmppConnection connection;
2649 private boolean carbonsEnabled = false;
2650 private boolean encryptionEnabled = false;
2651 private boolean blockListRequested = false;
2652
2653 public Features(final XmppConnection connection) {
2654 this.connection = connection;
2655 }
2656
2657 private boolean hasDiscoFeature(final Jid server, final String feature) {
2658 synchronized (XmppConnection.this.disco) {
2659 final ServiceDiscoveryResult sdr = connection.disco.get(server);
2660 return sdr != null && sdr.getFeatures().contains(feature);
2661 }
2662 }
2663
2664 public boolean carbons() {
2665 return hasDiscoFeature(account.getDomain(), Namespace.CARBONS);
2666 }
2667
2668 public boolean commands() {
2669 return hasDiscoFeature(account.getDomain(), Namespace.COMMANDS);
2670 }
2671
2672 public boolean easyOnboardingInvites() {
2673 synchronized (commands) {
2674 return commands.containsKey(Namespace.EASY_ONBOARDING_INVITE);
2675 }
2676 }
2677
2678 public boolean bookmarksConversion() {
2679 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS_CONVERSION)
2680 && pepPublishOptions();
2681 }
2682
2683 public boolean avatarConversion() {
2684 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.AVATAR_CONVERSION)
2685 && pepPublishOptions();
2686 }
2687
2688 public boolean blocking() {
2689 return hasDiscoFeature(account.getDomain(), Namespace.BLOCKING);
2690 }
2691
2692 public boolean spamReporting() {
2693 return hasDiscoFeature(account.getDomain(), Namespace.REPORTING);
2694 }
2695
2696 public boolean flexibleOfflineMessageRetrieval() {
2697 return hasDiscoFeature(
2698 account.getDomain(), Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL);
2699 }
2700
2701 public boolean register() {
2702 return hasDiscoFeature(account.getDomain(), Namespace.REGISTER);
2703 }
2704
2705 public boolean invite() {
2706 return connection.streamFeatures != null
2707 && connection.streamFeatures.hasChild("register", Namespace.INVITE);
2708 }
2709
2710 public boolean sm() {
2711 return streamId != null
2712 || (connection.streamFeatures != null
2713 && connection.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT));
2714 }
2715
2716 public boolean csi() {
2717 return connection.streamFeatures != null
2718 && connection.streamFeatures.hasChild("csi", Namespace.CSI);
2719 }
2720
2721 public boolean pep() {
2722 synchronized (XmppConnection.this.disco) {
2723 ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid());
2724 return info != null && info.hasIdentity("pubsub", "pep");
2725 }
2726 }
2727
2728 public boolean pepPersistent() {
2729 synchronized (XmppConnection.this.disco) {
2730 ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid());
2731 return info != null
2732 && info.getFeatures()
2733 .contains("http://jabber.org/protocol/pubsub#persistent-items");
2734 }
2735 }
2736
2737 public boolean pepPublishOptions() {
2738 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUBSUB_PUBLISH_OPTIONS);
2739 }
2740
2741 public boolean pepOmemoWhitelisted() {
2742 return hasDiscoFeature(
2743 account.getJid().asBareJid(), AxolotlService.PEP_OMEMO_WHITELISTED);
2744 }
2745
2746 public boolean mam() {
2747 return MessageArchiveService.Version.has(getAccountFeatures());
2748 }
2749
2750 public List<String> getAccountFeatures() {
2751 ServiceDiscoveryResult result = connection.disco.get(account.getJid().asBareJid());
2752 return result == null ? Collections.emptyList() : result.getFeatures();
2753 }
2754
2755 public boolean push() {
2756 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUSH)
2757 || hasDiscoFeature(account.getDomain(), Namespace.PUSH);
2758 }
2759
2760 public boolean rosterVersioning() {
2761 return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver");
2762 }
2763
2764 public void setBlockListRequested(boolean value) {
2765 this.blockListRequested = value;
2766 }
2767
2768 public boolean httpUpload(long filesize) {
2769 if (Config.DISABLE_HTTP_UPLOAD) {
2770 return false;
2771 } else {
2772 for (String namespace :
2773 new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) {
2774 List<Entry<Jid, ServiceDiscoveryResult>> items =
2775 findDiscoItemsByFeature(namespace);
2776 if (items.size() > 0) {
2777 try {
2778 long maxsize =
2779 Long.parseLong(
2780 items.get(0)
2781 .getValue()
2782 .getExtendedDiscoInformation(
2783 namespace, "max-file-size"));
2784 if (filesize <= maxsize) {
2785 return true;
2786 } else {
2787 Log.d(
2788 Config.LOGTAG,
2789 account.getJid().asBareJid()
2790 + ": http upload is not available for files with size "
2791 + filesize
2792 + " (max is "
2793 + maxsize
2794 + ")");
2795 return false;
2796 }
2797 } catch (Exception e) {
2798 return true;
2799 }
2800 }
2801 }
2802 return false;
2803 }
2804 }
2805
2806 public boolean useLegacyHttpUpload() {
2807 return findDiscoItemByFeature(Namespace.HTTP_UPLOAD) == null
2808 && findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY) != null;
2809 }
2810
2811 public long getMaxHttpUploadSize() {
2812 for (String namespace :
2813 new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) {
2814 List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(namespace);
2815 if (items.size() > 0) {
2816 try {
2817 return Long.parseLong(
2818 items.get(0)
2819 .getValue()
2820 .getExtendedDiscoInformation(namespace, "max-file-size"));
2821 } catch (Exception e) {
2822 // ignored
2823 }
2824 }
2825 }
2826 return -1;
2827 }
2828
2829 public boolean stanzaIds() {
2830 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.STANZA_IDS);
2831 }
2832
2833 public boolean bookmarks2() {
2834 return pepPublishOptions() && hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT);
2835 }
2836
2837 public boolean externalServiceDiscovery() {
2838 return hasDiscoFeature(account.getDomain(), Namespace.EXTERNAL_SERVICE_DISCOVERY);
2839 }
2840 }
2841}