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