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