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)) {authenticate(SaslMechanism.Version.SASL_2);
1418 } else if (isSecure && this.streamFeatures.hasChild("mechanisms", Namespace.SASL)) {
1419 authenticate(SaslMechanism.Version.SASL);
1420 } else {
1421 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1422 }
1423 }
1424
1425 private boolean isSecure() {
1426 return features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion();
1427 }
1428
1429 private void authenticate(final SaslMechanism.Version version) throws IOException {
1430 final Element authElement;
1431 if (version == SaslMechanism.Version.SASL) {
1432 authElement = this.streamFeatures.findChild("mechanisms", Namespace.SASL);
1433 } else {
1434 authElement = this.streamFeatures.findChild("authentication", Namespace.SASL_2);
1435 }
1436 final Collection<String> mechanisms = SaslMechanism.mechanisms(authElement);
1437 final Element cbElement =
1438 this.streamFeatures.findChild("sasl-channel-binding", Namespace.CHANNEL_BINDING);
1439 final Collection<ChannelBinding> channelBindings = ChannelBinding.of(cbElement);
1440 final SaslMechanism.Factory factory = new SaslMechanism.Factory(account);
1441 final SaslMechanism saslMechanism = factory.of(mechanisms, channelBindings, version, SSLSockets.version(this.socket));
1442 this.saslMechanism = validate(saslMechanism, mechanisms);
1443 final boolean quickStartAvailable;
1444 final String firstMessage = this.saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket));
1445 final boolean usingFast = SaslMechanism.hashedToken(this.saslMechanism);
1446 final Element authenticate;
1447 if (version == SaslMechanism.Version.SASL) {
1448 authenticate = new Element("auth", Namespace.SASL);
1449 if (!Strings.isNullOrEmpty(firstMessage)) {
1450 authenticate.setContent(firstMessage);
1451 }
1452 quickStartAvailable = false;
1453 } else if (version == SaslMechanism.Version.SASL_2) {
1454 final Element inline = authElement.findChild("inline", Namespace.SASL_2);
1455 final boolean sm = inline != null && inline.hasChild("sm", Namespace.STREAM_MANAGEMENT);
1456 final HashedToken.Mechanism hashTokenRequest;
1457 if (usingFast) {
1458 hashTokenRequest = null;
1459 } else {
1460 final Element fast = inline == null ? null : inline.findChild("fast", Namespace.FAST);
1461 final Collection<String> fastMechanisms = SaslMechanism.mechanisms(fast);
1462 hashTokenRequest =
1463 HashedToken.Mechanism.best(fastMechanisms, SSLSockets.version(this.socket));
1464 }
1465 final Collection<String> bindFeatures = Bind2.features(inline);
1466 quickStartAvailable =
1467 sm
1468 && bindFeatures != null
1469 && bindFeatures.containsAll(Bind2.QUICKSTART_FEATURES);
1470 if (bindFeatures != null) {
1471 try {
1472 mXmppConnectionService.restoredFromDatabaseLatch.await();
1473 } catch (final InterruptedException e) {
1474 Log.d(
1475 Config.LOGTAG,
1476 account.getJid().asBareJid()
1477 + ": interrupted while waiting for DB restore during SASL2 bind");
1478 return;
1479 }
1480 }
1481 this.hashTokenRequest = hashTokenRequest;
1482 authenticate = generateAuthenticationRequest(firstMessage, usingFast, hashTokenRequest, bindFeatures, sm);
1483 } else {
1484 throw new AssertionError("Missing implementation for " + version);
1485 }
1486
1487 if (account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, quickStartAvailable)) {
1488 mXmppConnectionService.databaseBackend.updateAccount(account);
1489 }
1490
1491 Log.d(
1492 Config.LOGTAG,
1493 account.getJid().toString()
1494 + ": Authenticating with "
1495 + version
1496 + "/"
1497 + this.saslMechanism.getMechanism());
1498 authenticate.setAttribute("mechanism", this.saslMechanism.getMechanism());
1499 synchronized (this.mStanzaQueue) {
1500 this.stanzasSentBeforeAuthentication = this.stanzasSent;
1501 tagWriter.writeElement(authenticate);
1502 }
1503 }
1504
1505 private static boolean isFastTokenAvailable(final Element authentication) {
1506 final Element inline = authentication == null ? null : authentication.findChild("inline");
1507 return inline != null && inline.hasChild("fast", Namespace.FAST);
1508 }
1509
1510 @NonNull
1511 private SaslMechanism validate(final @Nullable SaslMechanism saslMechanism, Collection<String> mechanisms) throws StateChangingException {
1512 if (saslMechanism == null) {
1513 Log.d(
1514 Config.LOGTAG,
1515 account.getJid().asBareJid()
1516 + ": unable to find supported SASL mechanism in "
1517 + mechanisms);
1518 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1519 }
1520 if (SaslMechanism.hashedToken(saslMechanism)) {
1521 return saslMechanism;
1522 }
1523 final int pinnedMechanism = account.getPinnedMechanismPriority();
1524 if (pinnedMechanism > saslMechanism.getPriority()) {
1525 Log.e(
1526 Config.LOGTAG,
1527 "Auth failed. Authentication mechanism "
1528 + saslMechanism.getMechanism()
1529 + " has lower priority ("
1530 + saslMechanism.getPriority()
1531 + ") than pinned priority ("
1532 + pinnedMechanism
1533 + "). Possible downgrade attack?");
1534 throw new StateChangingException(Account.State.DOWNGRADE_ATTACK);
1535 }
1536 return saslMechanism;
1537 }
1538
1539 private Element generateAuthenticationRequest(final String firstMessage, final boolean usingFast) {
1540 return generateAuthenticationRequest(firstMessage, usingFast, null, Bind2.QUICKSTART_FEATURES, true);
1541 }
1542
1543 private Element generateAuthenticationRequest(
1544 final String firstMessage,
1545 final boolean usingFast,
1546 final HashedToken.Mechanism hashedTokenRequest,
1547 final Collection<String> bind,
1548 final boolean inlineStreamManagement) {
1549 final Element authenticate = new Element("authenticate", Namespace.SASL_2);
1550 if (!Strings.isNullOrEmpty(firstMessage)) {
1551 authenticate.addChild("initial-response").setContent(firstMessage);
1552 }
1553 final Element userAgent = authenticate.addChild("user-agent");
1554 userAgent.setAttribute("id", AccountUtils.publicDeviceId(account));
1555 userAgent
1556 .addChild("software")
1557 .setContent(mXmppConnectionService.getString(R.string.app_name));
1558 if (!PhoneHelper.isEmulator()) {
1559 userAgent
1560 .addChild("device")
1561 .setContent(String.format("%s %s", Build.MANUFACTURER, Build.MODEL));
1562 }
1563 // do not include bind if 'inlinestreamManagment' is missing and we have a streamId
1564 final boolean mayAttemptBind = streamId == null || inlineStreamManagement;
1565 if (bind != null && mayAttemptBind) {
1566 authenticate.addChild(generateBindRequest(bind));
1567 }
1568 if (inlineStreamManagement && streamId != null) {
1569 final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived);
1570 this.mSmCatchupMessageCounter.set(0);
1571 this.mWaitingForSmCatchup.set(true);
1572 authenticate.addChild(resume);
1573 }
1574 if (hashedTokenRequest != null) {
1575 authenticate
1576 .addChild("request-token", Namespace.FAST)
1577 .setAttribute("mechanism", hashedTokenRequest.name());
1578 }
1579 if (usingFast) {
1580 authenticate.addChild("fast", Namespace.FAST);
1581 }
1582 return authenticate;
1583 }
1584
1585 private Element generateBindRequest(final Collection<String> bindFeatures) {
1586 Log.d(Config.LOGTAG, "inline bind features: " + bindFeatures);
1587 final Element bind = new Element("bind", Namespace.BIND2);
1588 bind.addChild("tag").setContent(mXmppConnectionService.getString(R.string.app_name));
1589 if (bindFeatures.contains(Namespace.CARBONS)) {
1590 bind.addChild("enable", Namespace.CARBONS);
1591 }
1592 if (bindFeatures.contains(Namespace.STREAM_MANAGEMENT)) {
1593 bind.addChild(new EnablePacket());
1594 }
1595 return bind;
1596 }
1597
1598 private void register() {
1599 final String preAuth = account.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN);
1600 if (preAuth != null && features.invite()) {
1601 final IqPacket preAuthRequest = new IqPacket(IqPacket.TYPE.SET);
1602 preAuthRequest.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth);
1603 sendUnmodifiedIqPacket(
1604 preAuthRequest,
1605 (account, response) -> {
1606 if (response.getType() == IqPacket.TYPE.RESULT) {
1607 sendRegistryRequest();
1608 } else {
1609 final String error = response.getErrorCondition();
1610 Log.d(
1611 Config.LOGTAG,
1612 account.getJid().asBareJid()
1613 + ": failed to pre auth. "
1614 + error);
1615 throw new StateChangingError(Account.State.REGISTRATION_INVALID_TOKEN);
1616 }
1617 },
1618 true);
1619 } else {
1620 sendRegistryRequest();
1621 }
1622 }
1623
1624 private void sendRegistryRequest() {
1625 final IqPacket register = new IqPacket(IqPacket.TYPE.GET);
1626 register.query(Namespace.REGISTER);
1627 register.setTo(account.getDomain());
1628 sendUnmodifiedIqPacket(
1629 register,
1630 (account, packet) -> {
1631 if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
1632 return;
1633 }
1634 if (packet.getType() == IqPacket.TYPE.ERROR) {
1635 throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1636 }
1637 final Element query = packet.query(Namespace.REGISTER);
1638 if (query.hasChild("username") && (query.hasChild("password"))) {
1639 final IqPacket register1 = new IqPacket(IqPacket.TYPE.SET);
1640 final Element username =
1641 new Element("username").setContent(account.getUsername());
1642 final Element password =
1643 new Element("password").setContent(account.getPassword());
1644 register1.query(Namespace.REGISTER).addChild(username);
1645 register1.query().addChild(password);
1646 register1.setFrom(account.getJid().asBareJid());
1647 sendUnmodifiedIqPacket(register1, registrationResponseListener, true);
1648 } else if (query.hasChild("x", Namespace.DATA)) {
1649 final Data data = Data.parse(query.findChild("x", Namespace.DATA));
1650 final Element blob = query.findChild("data", "urn:xmpp:bob");
1651 final String id = packet.getId();
1652 InputStream is;
1653 if (blob != null) {
1654 try {
1655 final String base64Blob = blob.getContent();
1656 final byte[] strBlob = Base64.decode(base64Blob, Base64.DEFAULT);
1657 is = new ByteArrayInputStream(strBlob);
1658 } catch (Exception e) {
1659 is = null;
1660 }
1661 } else {
1662 final boolean useTor =
1663 mXmppConnectionService.useTorToConnect() || account.isOnion();
1664 try {
1665 final String url = data.getValue("url");
1666 final String fallbackUrl = data.getValue("captcha-fallback-url");
1667 if (url != null) {
1668 is = HttpConnectionManager.open(url, useTor);
1669 } else if (fallbackUrl != null) {
1670 is = HttpConnectionManager.open(fallbackUrl, useTor);
1671 } else {
1672 is = null;
1673 }
1674 } catch (final IOException e) {
1675 Log.d(
1676 Config.LOGTAG,
1677 account.getJid().asBareJid() + ": unable to fetch captcha",
1678 e);
1679 is = null;
1680 }
1681 }
1682
1683 if (is != null) {
1684 Bitmap captcha = BitmapFactory.decodeStream(is);
1685 try {
1686 if (mXmppConnectionService.displayCaptchaRequest(
1687 account, id, data, captcha)) {
1688 return;
1689 }
1690 } catch (Exception e) {
1691 throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1692 }
1693 }
1694 throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1695 } else if (query.hasChild("instructions")
1696 || query.hasChild("x", Namespace.OOB)) {
1697 final String instructions = query.findChildContent("instructions");
1698 final Element oob = query.findChild("x", Namespace.OOB);
1699 final String url = oob == null ? null : oob.findChildContent("url");
1700 if (url != null) {
1701 setAccountCreationFailed(url);
1702 } else if (instructions != null) {
1703 final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(instructions);
1704 if (matcher.find()) {
1705 setAccountCreationFailed(
1706 instructions.substring(matcher.start(), matcher.end()));
1707 }
1708 }
1709 throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1710 }
1711 },
1712 true);
1713 }
1714
1715 private void setAccountCreationFailed(final String url) {
1716 final HttpUrl httpUrl = url == null ? null : HttpUrl.parse(url);
1717 if (httpUrl != null && httpUrl.isHttps()) {
1718 this.redirectionUrl = httpUrl;
1719 throw new StateChangingError(Account.State.REGISTRATION_WEB);
1720 }
1721 throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1722 }
1723
1724 public HttpUrl getRedirectionUrl() {
1725 return this.redirectionUrl;
1726 }
1727
1728 public void resetEverything() {
1729 resetAttemptCount(true);
1730 resetStreamId();
1731 clearIqCallbacks();
1732 this.stanzasSent = 0;
1733 mStanzaQueue.clear();
1734 this.redirectionUrl = null;
1735 synchronized (this.disco) {
1736 disco.clear();
1737 }
1738 synchronized (this.commands) {
1739 this.commands.clear();
1740 }
1741 this.saslMechanism = null;
1742 }
1743
1744 private void sendBindRequest() {
1745 try {
1746 mXmppConnectionService.restoredFromDatabaseLatch.await();
1747 } catch (InterruptedException e) {
1748 Log.d(
1749 Config.LOGTAG,
1750 account.getJid().asBareJid()
1751 + ": interrupted while waiting for DB restore during bind");
1752 return;
1753 }
1754 clearIqCallbacks();
1755 if (account.getJid().isBareJid()) {
1756 account.setResource(this.createNewResource());
1757 } else {
1758 fixResource(mXmppConnectionService, account);
1759 }
1760 final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
1761 final String resource =
1762 Config.USE_RANDOM_RESOURCE_ON_EVERY_BIND ? nextRandomId() : account.getResource();
1763 iq.addChild("bind", Namespace.BIND).addChild("resource").setContent(resource);
1764 this.sendUnmodifiedIqPacket(
1765 iq,
1766 (account, packet) -> {
1767 if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
1768 return;
1769 }
1770 final Element bind = packet.findChild("bind");
1771 if (bind != null && packet.getType() == IqPacket.TYPE.RESULT) {
1772 isBound = true;
1773 final Element jid = bind.findChild("jid");
1774 if (jid != null && jid.getContent() != null) {
1775 try {
1776 Jid assignedJid = Jid.ofEscaped(jid.getContent());
1777 if (!account.getJid().getDomain().equals(assignedJid.getDomain())) {
1778 Log.d(
1779 Config.LOGTAG,
1780 account.getJid().asBareJid()
1781 + ": server tried to re-assign domain to "
1782 + assignedJid.getDomain());
1783 throw new StateChangingError(Account.State.BIND_FAILURE);
1784 }
1785 if (account.setJid(assignedJid)) {
1786 Log.d(
1787 Config.LOGTAG,
1788 account.getJid().asBareJid()
1789 + ": jid changed during bind. updating database");
1790 mXmppConnectionService.databaseBackend.updateAccount(account);
1791 }
1792 if (streamFeatures.hasChild("session")
1793 && !streamFeatures
1794 .findChild("session")
1795 .hasChild("optional")) {
1796 sendStartSession();
1797 } else {
1798 final boolean waitForDisco = enableStreamManagement();
1799 sendPostBindInitialization(waitForDisco, false);
1800 }
1801 return;
1802 } catch (final IllegalArgumentException e) {
1803 Log.d(
1804 Config.LOGTAG,
1805 account.getJid().asBareJid()
1806 + ": server reported invalid jid ("
1807 + jid.getContent()
1808 + ") on bind");
1809 }
1810 } else {
1811 Log.d(
1812 Config.LOGTAG,
1813 account.getJid()
1814 + ": disconnecting because of bind failure. (no jid)");
1815 }
1816 } else {
1817 Log.d(
1818 Config.LOGTAG,
1819 account.getJid()
1820 + ": disconnecting because of bind failure ("
1821 + packet);
1822 }
1823 final Element error = packet.findChild("error");
1824 if (packet.getType() == IqPacket.TYPE.ERROR
1825 && error != null
1826 && error.hasChild("conflict")) {
1827 account.setResource(createNewResource());
1828 }
1829 throw new StateChangingError(Account.State.BIND_FAILURE);
1830 },
1831 true);
1832 }
1833
1834 private void clearIqCallbacks() {
1835 final IqPacket failurePacket = new IqPacket(IqPacket.TYPE.TIMEOUT);
1836 final ArrayList<OnIqPacketReceived> callbacks = new ArrayList<>();
1837 synchronized (this.packetCallbacks) {
1838 if (this.packetCallbacks.size() == 0) {
1839 return;
1840 }
1841 Log.d(
1842 Config.LOGTAG,
1843 account.getJid().asBareJid()
1844 + ": clearing "
1845 + this.packetCallbacks.size()
1846 + " iq callbacks");
1847 final Iterator<Pair<IqPacket, OnIqPacketReceived>> iterator =
1848 this.packetCallbacks.values().iterator();
1849 while (iterator.hasNext()) {
1850 Pair<IqPacket, OnIqPacketReceived> entry = iterator.next();
1851 callbacks.add(entry.second);
1852 iterator.remove();
1853 }
1854 }
1855 for (OnIqPacketReceived callback : callbacks) {
1856 try {
1857 callback.onIqPacketReceived(account, failurePacket);
1858 } catch (StateChangingError error) {
1859 Log.d(
1860 Config.LOGTAG,
1861 account.getJid().asBareJid()
1862 + ": caught StateChangingError("
1863 + error.state.toString()
1864 + ") while clearing callbacks");
1865 // ignore
1866 }
1867 }
1868 Log.d(
1869 Config.LOGTAG,
1870 account.getJid().asBareJid()
1871 + ": done clearing iq callbacks. "
1872 + this.packetCallbacks.size()
1873 + " left");
1874 }
1875
1876 public void sendDiscoTimeout() {
1877 if (mWaitForDisco.compareAndSet(true, false)) {
1878 Log.d(
1879 Config.LOGTAG,
1880 account.getJid().asBareJid() + ": finalizing bind after disco timeout");
1881 finalizeBind();
1882 }
1883 }
1884
1885 private void sendStartSession() {
1886 Log.d(
1887 Config.LOGTAG,
1888 account.getJid().asBareJid() + ": sending legacy session to outdated server");
1889 final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET);
1890 startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session");
1891 this.sendUnmodifiedIqPacket(
1892 startSession,
1893 (account, packet) -> {
1894 if (packet.getType() == IqPacket.TYPE.RESULT) {
1895 final boolean waitForDisco = enableStreamManagement();
1896 sendPostBindInitialization(waitForDisco, false);
1897 } else if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
1898 throw new StateChangingError(Account.State.SESSION_FAILURE);
1899 }
1900 },
1901 true);
1902 }
1903
1904 private boolean enableStreamManagement() {
1905 final boolean streamManagement =
1906 this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT);
1907 if (streamManagement) {
1908 synchronized (this.mStanzaQueue) {
1909 final EnablePacket enable = new EnablePacket();
1910 tagWriter.writeStanzaAsync(enable);
1911 stanzasSent = 0;
1912 mStanzaQueue.clear();
1913 }
1914 return true;
1915 } else {
1916 return false;
1917 }
1918 }
1919
1920 private void sendPostBindInitialization(
1921 final boolean waitForDisco, final boolean carbonsEnabled) {
1922 features.carbonsEnabled = carbonsEnabled;
1923 features.blockListRequested = false;
1924 synchronized (this.disco) {
1925 this.disco.clear();
1926 }
1927 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery");
1928 mPendingServiceDiscoveries.set(0);
1929 mWaitForDisco.set(waitForDisco);
1930 lastDiscoStarted = SystemClock.elapsedRealtime();
1931 mXmppConnectionService.scheduleWakeUpCall(
1932 Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
1933 final Element caps = streamFeatures.findChild("c");
1934 final String hash = caps == null ? null : caps.getAttribute("hash");
1935 final String ver = caps == null ? null : caps.getAttribute("ver");
1936 ServiceDiscoveryResult discoveryResult = null;
1937 if (hash != null && ver != null) {
1938 discoveryResult =
1939 mXmppConnectionService.getCachedServiceDiscoveryResult(new Pair<>(hash, ver));
1940 }
1941 final boolean requestDiscoItemsFirst =
1942 !account.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY);
1943 if (requestDiscoItemsFirst) {
1944 sendServiceDiscoveryItems(account.getDomain());
1945 }
1946 if (discoveryResult == null) {
1947 sendServiceDiscoveryInfo(account.getDomain());
1948 } else {
1949 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server caps came from cache");
1950 disco.put(account.getDomain(), discoveryResult);
1951 }
1952 discoverMamPreferences();
1953 sendServiceDiscoveryInfo(account.getJid().asBareJid());
1954 if (!requestDiscoItemsFirst) {
1955 sendServiceDiscoveryItems(account.getDomain());
1956 }
1957
1958 if (!mWaitForDisco.get()) {
1959 finalizeBind();
1960 }
1961 this.lastSessionStarted = SystemClock.elapsedRealtime();
1962 }
1963
1964 private void sendServiceDiscoveryInfo(final Jid jid) {
1965 mPendingServiceDiscoveries.incrementAndGet();
1966 final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
1967 iq.setTo(jid);
1968 iq.query("http://jabber.org/protocol/disco#info");
1969 this.sendIqPacket(
1970 iq,
1971 (account, packet) -> {
1972 if (packet.getType() == IqPacket.TYPE.RESULT) {
1973 boolean advancedStreamFeaturesLoaded;
1974 synchronized (XmppConnection.this.disco) {
1975 ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet);
1976 if (jid.equals(account.getDomain())) {
1977 mXmppConnectionService.databaseBackend.insertDiscoveryResult(
1978 result);
1979 }
1980 disco.put(jid, result);
1981 advancedStreamFeaturesLoaded =
1982 disco.containsKey(account.getDomain())
1983 && disco.containsKey(account.getJid().asBareJid());
1984 }
1985 if (advancedStreamFeaturesLoaded
1986 && (jid.equals(account.getDomain())
1987 || jid.equals(account.getJid().asBareJid()))) {
1988 enableAdvancedStreamFeatures();
1989 }
1990 } else if (packet.getType() == IqPacket.TYPE.ERROR) {
1991 Log.d(
1992 Config.LOGTAG,
1993 account.getJid().asBareJid()
1994 + ": could not query disco info for "
1995 + jid.toString());
1996 final boolean serverOrAccount =
1997 jid.equals(account.getDomain())
1998 || jid.equals(account.getJid().asBareJid());
1999 final boolean advancedStreamFeaturesLoaded;
2000 if (serverOrAccount) {
2001 synchronized (XmppConnection.this.disco) {
2002 disco.put(jid, ServiceDiscoveryResult.empty());
2003 advancedStreamFeaturesLoaded =
2004 disco.containsKey(account.getDomain())
2005 && disco.containsKey(account.getJid().asBareJid());
2006 }
2007 } else {
2008 advancedStreamFeaturesLoaded = false;
2009 }
2010 if (advancedStreamFeaturesLoaded) {
2011 enableAdvancedStreamFeatures();
2012 }
2013 }
2014 if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
2015 if (mPendingServiceDiscoveries.decrementAndGet() == 0
2016 && mWaitForDisco.compareAndSet(true, false)) {
2017 finalizeBind();
2018 }
2019 }
2020 });
2021 }
2022
2023 private void discoverMamPreferences() {
2024 IqPacket request = new IqPacket(IqPacket.TYPE.GET);
2025 request.addChild("prefs", MessageArchiveService.Version.MAM_2.namespace);
2026 sendIqPacket(
2027 request,
2028 (account, response) -> {
2029 if (response.getType() == IqPacket.TYPE.RESULT) {
2030 Element prefs =
2031 response.findChild(
2032 "prefs", MessageArchiveService.Version.MAM_2.namespace);
2033 isMamPreferenceAlways =
2034 "always"
2035 .equals(
2036 prefs == null
2037 ? null
2038 : prefs.getAttribute("default"));
2039 }
2040 });
2041 }
2042
2043 private void discoverCommands() {
2044 final IqPacket request = new IqPacket(IqPacket.TYPE.GET);
2045 request.setTo(account.getDomain());
2046 request.addChild("query", Namespace.DISCO_ITEMS).setAttribute("node", Namespace.COMMANDS);
2047 sendIqPacket(
2048 request,
2049 (account, response) -> {
2050 if (response.getType() == IqPacket.TYPE.RESULT) {
2051 final Element query = response.findChild("query", Namespace.DISCO_ITEMS);
2052 if (query == null) {
2053 return;
2054 }
2055 final HashMap<String, Jid> commands = new HashMap<>();
2056 for (final Element child : query.getChildren()) {
2057 if ("item".equals(child.getName())) {
2058 final String node = child.getAttribute("node");
2059 final Jid jid = child.getAttributeAsJid("jid");
2060 if (node != null && jid != null) {
2061 commands.put(node, jid);
2062 }
2063 }
2064 }
2065 synchronized (this.commands) {
2066 this.commands.clear();
2067 this.commands.putAll(commands);
2068 }
2069 }
2070 });
2071 }
2072
2073 public boolean isMamPreferenceAlways() {
2074 return isMamPreferenceAlways;
2075 }
2076
2077 private void finalizeBind() {
2078 if (bindListener != null) {
2079 bindListener.onBind(account);
2080 }
2081 changeStatusToOnline();
2082 }
2083
2084 private void enableAdvancedStreamFeatures() {
2085 if (getFeatures().blocking() && !features.blockListRequested) {
2086 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Requesting block list");
2087 this.sendIqPacket(
2088 getIqGenerator().generateGetBlockList(), mXmppConnectionService.getIqParser());
2089 }
2090 for (final OnAdvancedStreamFeaturesLoaded listener :
2091 advancedStreamFeaturesLoadedListeners) {
2092 listener.onAdvancedStreamFeaturesAvailable(account);
2093 }
2094 if (getFeatures().carbons() && !features.carbonsEnabled) {
2095 sendEnableCarbons();
2096 }
2097 if (getFeatures().commands()) {
2098 discoverCommands();
2099 }
2100 }
2101
2102 private void sendServiceDiscoveryItems(final Jid server) {
2103 mPendingServiceDiscoveries.incrementAndGet();
2104 final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
2105 iq.setTo(server.getDomain());
2106 iq.query("http://jabber.org/protocol/disco#items");
2107 this.sendIqPacket(
2108 iq,
2109 (account, packet) -> {
2110 if (packet.getType() == IqPacket.TYPE.RESULT) {
2111 final HashSet<Jid> items = new HashSet<>();
2112 final List<Element> elements = packet.query().getChildren();
2113 for (final Element element : elements) {
2114 if (element.getName().equals("item")) {
2115 final Jid jid =
2116 InvalidJid.getNullForInvalid(
2117 element.getAttributeAsJid("jid"));
2118 if (jid != null && !jid.equals(account.getDomain())) {
2119 items.add(jid);
2120 }
2121 }
2122 }
2123 for (Jid jid : items) {
2124 sendServiceDiscoveryInfo(jid);
2125 }
2126 } else {
2127 Log.d(
2128 Config.LOGTAG,
2129 account.getJid().asBareJid()
2130 + ": could not query disco items of "
2131 + server);
2132 }
2133 if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
2134 if (mPendingServiceDiscoveries.decrementAndGet() == 0
2135 && mWaitForDisco.compareAndSet(true, false)) {
2136 finalizeBind();
2137 }
2138 }
2139 });
2140 }
2141
2142 private void sendEnableCarbons() {
2143 final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
2144 iq.addChild("enable", Namespace.CARBONS);
2145 this.sendIqPacket(
2146 iq,
2147 (account, packet) -> {
2148 if (packet.getType() == IqPacket.TYPE.RESULT) {
2149 Log.d(
2150 Config.LOGTAG,
2151 account.getJid().asBareJid() + ": successfully enabled carbons");
2152 features.carbonsEnabled = true;
2153 } else {
2154 Log.d(
2155 Config.LOGTAG,
2156 account.getJid().asBareJid()
2157 + ": could not enable carbons "
2158 + packet);
2159 }
2160 });
2161 }
2162
2163 private void processStreamError(final Tag currentTag) throws IOException {
2164 final Element streamError = tagReader.readElement(currentTag);
2165 if (streamError == null) {
2166 return;
2167 }
2168 if (streamError.hasChild("conflict")) {
2169 account.setResource(createNewResource());
2170 Log.d(
2171 Config.LOGTAG,
2172 account.getJid().asBareJid()
2173 + ": switching resource due to conflict ("
2174 + account.getResource()
2175 + ")");
2176 throw new IOException();
2177 } else if (streamError.hasChild("host-unknown")) {
2178 throw new StateChangingException(Account.State.HOST_UNKNOWN);
2179 } else if (streamError.hasChild("policy-violation")) {
2180 this.lastConnect = SystemClock.elapsedRealtime();
2181 final String text = streamError.findChildContent("text");
2182 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": policy violation. " + text);
2183 failPendingMessages(text);
2184 throw new StateChangingException(Account.State.POLICY_VIOLATION);
2185 } else if (streamError.hasChild("see-other-host")) {
2186 final String seeOtherHost = streamError.findChildContent("see-other-host");
2187 final Resolver.Result currentResolverResult = this.currentResolverResult;
2188 if (Strings.isNullOrEmpty(seeOtherHost) || currentResolverResult == null) {
2189 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError);
2190 throw new StateChangingException(Account.State.STREAM_ERROR);
2191 }
2192 Log.d(Config.LOGTAG,account.getJid().asBareJid()+": see other host: "+seeOtherHost+" "+currentResolverResult);
2193 final Resolver.Result seeOtherResult = currentResolverResult.seeOtherHost(seeOtherHost);
2194 if (seeOtherResult != null) {
2195 this.seeOtherHostResolverResult = seeOtherResult;
2196 throw new StateChangingException(Account.State.SEE_OTHER_HOST);
2197 } else {
2198 throw new StateChangingException(Account.State.STREAM_ERROR);
2199 }
2200 } else {
2201 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError);
2202 throw new StateChangingException(Account.State.STREAM_ERROR);
2203 }
2204 }
2205
2206 private void failPendingMessages(final String error) {
2207 synchronized (this.mStanzaQueue) {
2208 for (int i = 0; i < mStanzaQueue.size(); ++i) {
2209 final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i);
2210 if (stanza instanceof MessagePacket) {
2211 final MessagePacket packet = (MessagePacket) stanza;
2212 final String id = packet.getId();
2213 final Jid to = packet.getTo();
2214 mXmppConnectionService.markMessage(
2215 account, to.asBareJid(), id, Message.STATUS_SEND_FAILED, error);
2216 }
2217 }
2218 }
2219 }
2220
2221 private boolean establishStream(final SSLSockets.Version sslVersion)
2222 throws IOException, InterruptedException {
2223 final boolean secureConnection = sslVersion != SSLSockets.Version.NONE;
2224 final SaslMechanism quickStartMechanism;
2225 if (secureConnection) {
2226 quickStartMechanism = SaslMechanism.ensureAvailable(account.getQuickStartMechanism(), sslVersion);
2227 } else {
2228 quickStartMechanism = null;
2229 }
2230 if (secureConnection
2231 && Config.QUICKSTART_ENABLED
2232 && quickStartMechanism != null
2233 && account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) {
2234 mXmppConnectionService.restoredFromDatabaseLatch.await();
2235 this.saslMechanism = quickStartMechanism;
2236 final boolean usingFast = quickStartMechanism instanceof HashedToken;
2237 final Element authenticate =
2238 generateAuthenticationRequest(quickStartMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)), usingFast);
2239 authenticate.setAttribute("mechanism", quickStartMechanism.getMechanism());
2240 sendStartStream(true, false);
2241 synchronized (this.mStanzaQueue) {
2242 this.stanzasSentBeforeAuthentication = this.stanzasSent;
2243 tagWriter.writeElement(authenticate);
2244 }
2245 Log.d(
2246 Config.LOGTAG,
2247 account.getJid().toString()
2248 + ": quick start with "
2249 + quickStartMechanism.getMechanism());
2250 return true;
2251 } else {
2252 sendStartStream(secureConnection, true);
2253 return false;
2254 }
2255 }
2256
2257 private void sendStartStream(final boolean from, final boolean flush) throws IOException {
2258 final Tag stream = Tag.start("stream:stream");
2259 stream.setAttribute("to", account.getServer());
2260 if (from) {
2261 stream.setAttribute("from", account.getJid().asBareJid().toEscapedString());
2262 }
2263 stream.setAttribute("version", "1.0");
2264 stream.setAttribute("xml:lang", LocalizedContent.STREAM_LANGUAGE);
2265 stream.setAttribute("xmlns", "jabber:client");
2266 stream.setAttribute("xmlns:stream", Namespace.STREAMS);
2267 tagWriter.writeTag(stream, flush);
2268 }
2269
2270 private String createNewResource() {
2271 return mXmppConnectionService.getString(R.string.app_name) + '.' + nextRandomId(true);
2272 }
2273
2274 private String nextRandomId() {
2275 return nextRandomId(false);
2276 }
2277
2278 private String nextRandomId(final boolean s) {
2279 return CryptoHelper.random(s ? 3 : 9);
2280 }
2281
2282 public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) {
2283 packet.setFrom(account.getJid());
2284 return this.sendUnmodifiedIqPacket(packet, callback, false);
2285 }
2286
2287 public synchronized String sendUnmodifiedIqPacket(
2288 final IqPacket packet, final OnIqPacketReceived callback, boolean force) {
2289 if (packet.getId() == null) {
2290 packet.setAttribute("id", nextRandomId());
2291 }
2292 if (callback != null) {
2293 synchronized (this.packetCallbacks) {
2294 packetCallbacks.put(packet.getId(), new Pair<>(packet, callback));
2295 }
2296 }
2297 this.sendPacket(packet, force);
2298 return packet.getId();
2299 }
2300
2301 public void sendMessagePacket(final MessagePacket packet) {
2302 this.sendPacket(packet);
2303 }
2304
2305 public void sendPresencePacket(final PresencePacket packet) {
2306 this.sendPacket(packet);
2307 }
2308
2309 private synchronized void sendPacket(final AbstractStanza packet) {
2310 sendPacket(packet, false);
2311 }
2312
2313 private synchronized void sendPacket(final AbstractStanza packet, final boolean force) {
2314 if (stanzasSent == Integer.MAX_VALUE) {
2315 resetStreamId();
2316 disconnect(true);
2317 return;
2318 }
2319 synchronized (this.mStanzaQueue) {
2320 if (force || isBound) {
2321 tagWriter.writeStanzaAsync(packet);
2322 } else {
2323 Log.d(
2324 Config.LOGTAG,
2325 account.getJid().asBareJid()
2326 + " do not write stanza to unbound stream "
2327 + packet.toString());
2328 }
2329 if (packet instanceof AbstractAcknowledgeableStanza) {
2330 AbstractAcknowledgeableStanza stanza = (AbstractAcknowledgeableStanza) packet;
2331
2332 if (this.mStanzaQueue.size() != 0) {
2333 int currentHighestKey = this.mStanzaQueue.keyAt(this.mStanzaQueue.size() - 1);
2334 if (currentHighestKey != stanzasSent) {
2335 throw new AssertionError("Stanza count messed up");
2336 }
2337 }
2338
2339 ++stanzasSent;
2340 if (Config.EXTENDED_SM_LOGGING) {
2341 Log.d(Config.LOGTAG, account.getJid().asBareJid()+": counting outbound "+packet.getName()+" as #" + stanzasSent);
2342 }
2343 this.mStanzaQueue.append(stanzasSent, stanza);
2344 if (stanza instanceof MessagePacket && stanza.getId() != null && inSmacksSession) {
2345 if (Config.EXTENDED_SM_LOGGING) {
2346 Log.d(
2347 Config.LOGTAG,
2348 account.getJid().asBareJid()
2349 + ": requesting ack for message stanza #"
2350 + stanzasSent);
2351 }
2352 tagWriter.writeStanzaAsync(new RequestPacket());
2353 }
2354 }
2355 }
2356 }
2357
2358 public void sendPing() {
2359 if (!r()) {
2360 final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
2361 iq.setFrom(account.getJid());
2362 iq.addChild("ping", Namespace.PING);
2363 this.sendIqPacket(iq, null);
2364 }
2365 this.lastPingSent = SystemClock.elapsedRealtime();
2366 }
2367
2368 public void setOnMessagePacketReceivedListener(final OnMessagePacketReceived listener) {
2369 this.messageListener = listener;
2370 }
2371
2372 public void setOnUnregisteredIqPacketReceivedListener(final OnIqPacketReceived listener) {
2373 this.unregisteredIqListener = listener;
2374 }
2375
2376 public void setOnPresencePacketReceivedListener(final OnPresencePacketReceived listener) {
2377 this.presenceListener = listener;
2378 }
2379
2380 public void setOnJinglePacketReceivedListener(final OnJinglePacketReceived listener) {
2381 this.jingleListener = listener;
2382 }
2383
2384 public void setOnStatusChangedListener(final OnStatusChanged listener) {
2385 this.statusListener = listener;
2386 }
2387
2388 public void setOnBindListener(final OnBindListener listener) {
2389 this.bindListener = listener;
2390 }
2391
2392 public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) {
2393 this.acknowledgedListener = listener;
2394 }
2395
2396 public void addOnAdvancedStreamFeaturesAvailableListener(
2397 final OnAdvancedStreamFeaturesLoaded listener) {
2398 this.advancedStreamFeaturesLoadedListeners.add(listener);
2399 }
2400
2401 private void forceCloseSocket() {
2402 FileBackend.close(this.socket);
2403 FileBackend.close(this.tagReader);
2404 }
2405
2406 public void interrupt() {
2407 if (this.mThread != null) {
2408 this.mThread.interrupt();
2409 }
2410 }
2411
2412 public void disconnect(final boolean force) {
2413 interrupt();
2414 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": disconnecting force=" + force);
2415 if (force) {
2416 forceCloseSocket();
2417 } else {
2418 final TagWriter currentTagWriter = this.tagWriter;
2419 if (currentTagWriter.isActive()) {
2420 currentTagWriter.finish();
2421 final Socket currentSocket = this.socket;
2422 final CountDownLatch streamCountDownLatch = this.mStreamCountDownLatch;
2423 try {
2424 currentTagWriter.await(1, TimeUnit.SECONDS);
2425 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": closing stream");
2426 currentTagWriter.writeTag(Tag.end("stream:stream"));
2427 if (streamCountDownLatch != null) {
2428 if (streamCountDownLatch.await(1, TimeUnit.SECONDS)) {
2429 Log.d(
2430 Config.LOGTAG,
2431 account.getJid().asBareJid() + ": remote ended stream");
2432 } else {
2433 Log.d(
2434 Config.LOGTAG,
2435 account.getJid().asBareJid()
2436 + ": remote has not closed socket. force closing");
2437 }
2438 }
2439 } catch (InterruptedException e) {
2440 Log.d(
2441 Config.LOGTAG,
2442 account.getJid().asBareJid()
2443 + ": interrupted while gracefully closing stream");
2444 } catch (final IOException e) {
2445 Log.d(
2446 Config.LOGTAG,
2447 account.getJid().asBareJid()
2448 + ": io exception during disconnect ("
2449 + e.getMessage()
2450 + ")");
2451 } finally {
2452 FileBackend.close(currentSocket);
2453 }
2454 } else {
2455 forceCloseSocket();
2456 }
2457 }
2458 }
2459
2460 private void resetStreamId() {
2461 this.streamId = null;
2462 this.boundStreamFeatures = null;
2463 }
2464
2465 private List<Entry<Jid, ServiceDiscoveryResult>> findDiscoItemsByFeature(final String feature) {
2466 synchronized (this.disco) {
2467 final List<Entry<Jid, ServiceDiscoveryResult>> items = new ArrayList<>();
2468 for (final Entry<Jid, ServiceDiscoveryResult> cursor : this.disco.entrySet()) {
2469 if (cursor.getValue().getFeatures().contains(feature)) {
2470 items.add(cursor);
2471 }
2472 }
2473 return items;
2474 }
2475 }
2476
2477 public Jid findDiscoItemByFeature(final String feature) {
2478 final List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(feature);
2479 if (items.size() >= 1) {
2480 return items.get(0).getKey();
2481 }
2482 return null;
2483 }
2484
2485 public boolean r() {
2486 if (getFeatures().sm()) {
2487 this.tagWriter.writeStanzaAsync(new RequestPacket());
2488 return true;
2489 } else {
2490 return false;
2491 }
2492 }
2493
2494 public List<String> getMucServersWithholdAccount() {
2495 final List<String> servers = getMucServers();
2496 servers.remove(account.getDomain().toEscapedString());
2497 return servers;
2498 }
2499
2500 public List<String> getMucServers() {
2501 List<String> servers = new ArrayList<>();
2502 synchronized (this.disco) {
2503 for (final Entry<Jid, ServiceDiscoveryResult> cursor : disco.entrySet()) {
2504 final ServiceDiscoveryResult value = cursor.getValue();
2505 if (value.getFeatures().contains("http://jabber.org/protocol/muc")
2506 && value.hasIdentity("conference", "text")
2507 && !value.getFeatures().contains("jabber:iq:gateway")
2508 && !value.hasIdentity("conference", "irc")) {
2509 servers.add(cursor.getKey().toString());
2510 }
2511 }
2512 }
2513 return servers;
2514 }
2515
2516 public String getMucServer() {
2517 List<String> servers = getMucServers();
2518 return servers.size() > 0 ? servers.get(0) : null;
2519 }
2520
2521 public int getTimeToNextAttempt(final boolean aggressive) {
2522 final int interval;
2523 if (aggressive) {
2524 interval = Math.min((int) (3 * Math.pow(1.3,attempt)), 60);
2525 } else {
2526 final int additionalTime =
2527 account.getLastErrorStatus() == Account.State.POLICY_VIOLATION ? 3 : 0;
2528 interval = Math.min((int) (25 * Math.pow(1.3, (additionalTime + attempt))), 300);
2529 }
2530 final int secondsSinceLast =
2531 (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
2532 return interval - secondsSinceLast;
2533 }
2534
2535 public int getAttempt() {
2536 return this.attempt;
2537 }
2538
2539 public Features getFeatures() {
2540 return this.features;
2541 }
2542
2543 public long getLastSessionEstablished() {
2544 final long diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
2545 return System.currentTimeMillis() - diff;
2546 }
2547
2548 public long getLastConnect() {
2549 return this.lastConnect;
2550 }
2551
2552 public long getLastPingSent() {
2553 return this.lastPingSent;
2554 }
2555
2556 public long getLastDiscoStarted() {
2557 return this.lastDiscoStarted;
2558 }
2559
2560 public long getLastPacketReceived() {
2561 return this.lastPacketReceived;
2562 }
2563
2564 public void sendActive() {
2565 this.sendPacket(new ActivePacket());
2566 }
2567
2568 public void sendInactive() {
2569 this.sendPacket(new InactivePacket());
2570 }
2571
2572 public void resetAttemptCount(boolean resetConnectTime) {
2573 this.attempt = 0;
2574 if (resetConnectTime) {
2575 this.lastConnect = 0;
2576 }
2577 }
2578
2579 public void setInteractive(boolean interactive) {
2580 this.mInteractive = interactive;
2581 }
2582
2583 private IqGenerator getIqGenerator() {
2584 return mXmppConnectionService.getIqGenerator();
2585 }
2586
2587 private class MyKeyManager implements X509KeyManager {
2588 @Override
2589 public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
2590 return account.getPrivateKeyAlias();
2591 }
2592
2593 @Override
2594 public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
2595 return null;
2596 }
2597
2598 @Override
2599 public X509Certificate[] getCertificateChain(String alias) {
2600 Log.d(Config.LOGTAG, "getting certificate chain");
2601 try {
2602 return KeyChain.getCertificateChain(mXmppConnectionService, alias);
2603 } catch (final Exception e) {
2604 Log.d(Config.LOGTAG, "could not get certificate chain", e);
2605 return new X509Certificate[0];
2606 }
2607 }
2608
2609 @Override
2610 public String[] getClientAliases(String s, Principal[] principals) {
2611 final String alias = account.getPrivateKeyAlias();
2612 return alias != null ? new String[] {alias} : new String[0];
2613 }
2614
2615 @Override
2616 public String[] getServerAliases(String s, Principal[] principals) {
2617 return new String[0];
2618 }
2619
2620 @Override
2621 public PrivateKey getPrivateKey(String alias) {
2622 try {
2623 return KeyChain.getPrivateKey(mXmppConnectionService, alias);
2624 } catch (Exception e) {
2625 return null;
2626 }
2627 }
2628 }
2629
2630 private static class StateChangingError extends Error {
2631 private final Account.State state;
2632
2633 public StateChangingError(Account.State state) {
2634 this.state = state;
2635 }
2636 }
2637
2638 private static class StateChangingException extends IOException {
2639 private final Account.State state;
2640
2641 public StateChangingException(Account.State state) {
2642 this.state = state;
2643 }
2644 }
2645
2646 public class Features {
2647 XmppConnection connection;
2648 private boolean carbonsEnabled = false;
2649 private boolean encryptionEnabled = false;
2650 private boolean blockListRequested = false;
2651
2652 public Features(final XmppConnection connection) {
2653 this.connection = connection;
2654 }
2655
2656 private boolean hasDiscoFeature(final Jid server, final String feature) {
2657 synchronized (XmppConnection.this.disco) {
2658 final ServiceDiscoveryResult sdr = connection.disco.get(server);
2659 return sdr != null && sdr.getFeatures().contains(feature);
2660 }
2661 }
2662
2663 public boolean carbons() {
2664 return hasDiscoFeature(account.getDomain(), Namespace.CARBONS);
2665 }
2666
2667 public boolean commands() {
2668 return hasDiscoFeature(account.getDomain(), Namespace.COMMANDS);
2669 }
2670
2671 public boolean easyOnboardingInvites() {
2672 synchronized (commands) {
2673 return commands.containsKey(Namespace.EASY_ONBOARDING_INVITE);
2674 }
2675 }
2676
2677 public boolean bookmarksConversion() {
2678 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS_CONVERSION)
2679 && pepPublishOptions();
2680 }
2681
2682 public boolean avatarConversion() {
2683 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.AVATAR_CONVERSION)
2684 && pepPublishOptions();
2685 }
2686
2687 public boolean blocking() {
2688 return hasDiscoFeature(account.getDomain(), Namespace.BLOCKING);
2689 }
2690
2691 public boolean spamReporting() {
2692 return hasDiscoFeature(account.getDomain(), Namespace.REPORTING);
2693 }
2694
2695 public boolean flexibleOfflineMessageRetrieval() {
2696 return hasDiscoFeature(
2697 account.getDomain(), Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL);
2698 }
2699
2700 public boolean register() {
2701 return hasDiscoFeature(account.getDomain(), Namespace.REGISTER);
2702 }
2703
2704 public boolean invite() {
2705 return connection.streamFeatures != null
2706 && connection.streamFeatures.hasChild("register", Namespace.INVITE);
2707 }
2708
2709 public boolean sm() {
2710 return streamId != null
2711 || (connection.streamFeatures != null
2712 && connection.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT));
2713 }
2714
2715 public boolean csi() {
2716 return connection.streamFeatures != null
2717 && connection.streamFeatures.hasChild("csi", Namespace.CSI);
2718 }
2719
2720 public boolean pep() {
2721 synchronized (XmppConnection.this.disco) {
2722 ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid());
2723 return info != null && info.hasIdentity("pubsub", "pep");
2724 }
2725 }
2726
2727 public boolean pepPersistent() {
2728 synchronized (XmppConnection.this.disco) {
2729 ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid());
2730 return info != null
2731 && info.getFeatures()
2732 .contains("http://jabber.org/protocol/pubsub#persistent-items");
2733 }
2734 }
2735
2736 public boolean pepPublishOptions() {
2737 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUBSUB_PUBLISH_OPTIONS);
2738 }
2739
2740 public boolean pepOmemoWhitelisted() {
2741 return hasDiscoFeature(
2742 account.getJid().asBareJid(), AxolotlService.PEP_OMEMO_WHITELISTED);
2743 }
2744
2745 public boolean mam() {
2746 return MessageArchiveService.Version.has(getAccountFeatures());
2747 }
2748
2749 public List<String> getAccountFeatures() {
2750 ServiceDiscoveryResult result = connection.disco.get(account.getJid().asBareJid());
2751 return result == null ? Collections.emptyList() : result.getFeatures();
2752 }
2753
2754 public boolean push() {
2755 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUSH)
2756 || hasDiscoFeature(account.getDomain(), Namespace.PUSH);
2757 }
2758
2759 public boolean rosterVersioning() {
2760 return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver");
2761 }
2762
2763 public void setBlockListRequested(boolean value) {
2764 this.blockListRequested = value;
2765 }
2766
2767 public boolean httpUpload(long filesize) {
2768 if (Config.DISABLE_HTTP_UPLOAD) {
2769 return false;
2770 } else {
2771 for (String namespace :
2772 new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) {
2773 List<Entry<Jid, ServiceDiscoveryResult>> items =
2774 findDiscoItemsByFeature(namespace);
2775 if (items.size() > 0) {
2776 try {
2777 long maxsize =
2778 Long.parseLong(
2779 items.get(0)
2780 .getValue()
2781 .getExtendedDiscoInformation(
2782 namespace, "max-file-size"));
2783 if (filesize <= maxsize) {
2784 return true;
2785 } else {
2786 Log.d(
2787 Config.LOGTAG,
2788 account.getJid().asBareJid()
2789 + ": http upload is not available for files with size "
2790 + filesize
2791 + " (max is "
2792 + maxsize
2793 + ")");
2794 return false;
2795 }
2796 } catch (Exception e) {
2797 return true;
2798 }
2799 }
2800 }
2801 return false;
2802 }
2803 }
2804
2805 public boolean useLegacyHttpUpload() {
2806 return findDiscoItemByFeature(Namespace.HTTP_UPLOAD) == null
2807 && findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY) != null;
2808 }
2809
2810 public long getMaxHttpUploadSize() {
2811 for (String namespace :
2812 new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) {
2813 List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(namespace);
2814 if (items.size() > 0) {
2815 try {
2816 return Long.parseLong(
2817 items.get(0)
2818 .getValue()
2819 .getExtendedDiscoInformation(namespace, "max-file-size"));
2820 } catch (Exception e) {
2821 // ignored
2822 }
2823 }
2824 }
2825 return -1;
2826 }
2827
2828 public boolean stanzaIds() {
2829 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.STANZA_IDS);
2830 }
2831
2832 public boolean bookmarks2() {
2833 return pepPublishOptions() && hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT);
2834 }
2835
2836 public boolean externalServiceDiscovery() {
2837 return hasDiscoFeature(account.getDomain(), Namespace.EXTERNAL_SERVICE_DISCOVERY);
2838 }
2839 }
2840}