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