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