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", Namespace.STREAMS)) {
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 Tag tag = tagReader.readTag();
709 if (tag != null && tag.isStart("features", Namespace.STREAMS)) {
710 this.streamFeatures = tagReader.readElement(tag);
711 Log.d(
712 Config.LOGTAG,
713 account.getJid().asBareJid()
714 + ": processed NOP stream features after success "
715 + XmlHelper.printElementNames(this.streamFeatures));
716 } else {
717 Log.d(
718 Config.LOGTAG,
719 account.getJid().asBareJid()
720 + ": server did not send stream features after SASL2 success");
721 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
722 }
723 Log.d(Config.LOGTAG, "success: " + success);
724 final String authorizationIdentifier =
725 success.findChildContent("authorization-identifier");
726 final Jid authorizationJid;
727 try {
728 authorizationJid =
729 Strings.isNullOrEmpty(authorizationIdentifier)
730 ? null
731 : Jid.ofEscaped(authorizationIdentifier);
732 } catch (final IllegalArgumentException e) {
733 Log.d(
734 Config.LOGTAG,
735 account.getJid().asBareJid()
736 + ": SASL 2.0 authorization identifier was not a valid jid");
737 throw new StateChangingException(Account.State.BIND_FAILURE);
738 }
739 if (authorizationJid == null) {
740 throw new StateChangingException(Account.State.BIND_FAILURE);
741 }
742 Log.d(
743 Config.LOGTAG,
744 account.getJid().asBareJid()
745 + ": SASL 2.0 authorization identifier was "
746 + authorizationJid);
747 if (!account.getJid().getDomain().equals(authorizationJid.getDomain())) {
748 Log.d(
749 Config.LOGTAG,
750 account.getJid().asBareJid()
751 + ": server tried to re-assign domain to "
752 + authorizationJid.getDomain());
753 throw new StateChangingError(Account.State.BIND_FAILURE);
754 }
755 if (authorizationJid.isFullJid() && account.setJid(authorizationJid)) {
756 Log.d(
757 Config.LOGTAG,
758 account.getJid().asBareJid()
759 + ": jid changed during SASL 2.0. updating database");
760 mXmppConnectionService.databaseBackend.updateAccount(account);
761 }
762 final Element bound = success.findChild("bound", Namespace.BIND2);
763 final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3");
764 final Element failed = success.findChild("failed", "urn:xmpp:sm:3");
765 if (bound != null && resumed != null) {
766 Log.d(
767 Config.LOGTAG,
768 account.getJid().asBareJid()
769 + ": server sent bound and resumed in SASL2 success");
770 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
771 }
772 if (resumed != null && streamId != null) {
773 processResumed(resumed);
774 } else if (failed != null) {
775 processFailed(failed, false); // wait for new stream features
776 }
777 if (bound != null) {
778 clearIqCallbacks();
779 this.isBound = true;
780 final Element streamManagementEnabled =
781 bound.findChild("enabled", Namespace.STREAM_MANAGEMENT);
782 final Element carbonsEnabled = bound.findChild("enabled", Namespace.CARBONS);
783 if (streamManagementEnabled != null) {
784 processEnabled(streamManagementEnabled);
785 }
786 if (carbonsEnabled != null) {
787 Log.d(
788 Config.LOGTAG,
789 account.getJid().asBareJid() + ": successfully enabled carbons");
790 features.carbonsEnabled = true;
791 }
792 // TODO if we didn’t enable stream managment in bind do it now
793 // TODO if both are set mark account ready for pipelining
794 sendPostBindInitialization(streamManagementEnabled != null, carbonsEnabled != null);
795 }
796 }
797 this.quickStartInProgress = false;
798 if (version == SaslMechanism.Version.SASL) {
799 tagReader.reset();
800 sendStartStream(true);
801 final Tag tag = tagReader.readTag();
802 if (tag != null && tag.isStart("stream", Namespace.STREAMS)) {
803 processStream();
804 return true;
805 } else {
806 throw new StateChangingException(Account.State.STREAM_OPENING_ERROR);
807 }
808 } else {
809 return false;
810 }
811 }
812
813 private void processFailure(final Element failure) throws StateChangingException {
814 final SaslMechanism.Version version;
815 try {
816 version = SaslMechanism.Version.of(failure);
817 } catch (final IllegalArgumentException e) {
818 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
819 }
820 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version);
821 if (failure.hasChild("temporary-auth-failure")) {
822 throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE);
823 } else if (failure.hasChild("account-disabled")) {
824 final String text = failure.findChildContent("text");
825 if (Strings.isNullOrEmpty(text)) {
826 throw new StateChangingException(Account.State.UNAUTHORIZED);
827 }
828 final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(text);
829 if (matcher.find()) {
830 final HttpUrl url;
831 try {
832 url = HttpUrl.get(text.substring(matcher.start(), matcher.end()));
833 } catch (final IllegalArgumentException e) {
834 throw new StateChangingException(Account.State.UNAUTHORIZED);
835 }
836 if (url.isHttps()) {
837 this.redirectionUrl = url;
838 throw new StateChangingException(Account.State.PAYMENT_REQUIRED);
839 }
840 }
841 }
842 throw new StateChangingException(Account.State.UNAUTHORIZED);
843 }
844
845 private static SSLSocket sslSocketOrNull(final Socket socket) {
846 if (socket instanceof SSLSocket) {
847 return (SSLSocket) socket;
848 } else {
849 return null;
850 }
851 }
852
853 private void processEnabled(final Element enabled) {
854 final String streamId;
855 if (enabled.getAttributeAsBoolean("resume")) {
856 streamId = enabled.getAttribute("id");
857 Log.d(
858 Config.LOGTAG,
859 account.getJid().asBareJid().toString()
860 + ": stream management enabled (resumable)");
861 } else {
862 Log.d(
863 Config.LOGTAG,
864 account.getJid().asBareJid().toString() + ": stream management enabled");
865 streamId = null;
866 }
867 this.streamId = streamId;
868 this.stanzasReceived = 0;
869 this.inSmacksSession = true;
870 final RequestPacket r = new RequestPacket();
871 tagWriter.writeStanzaAsync(r);
872 }
873
874 private void processResumed(final Element resumed) throws StateChangingException {
875 this.inSmacksSession = true;
876 this.isBound = true;
877 this.tagWriter.writeStanzaAsync(new RequestPacket());
878 lastPacketReceived = SystemClock.elapsedRealtime();
879 final String h = resumed.getAttribute("h");
880 if (h == null) {
881 resetStreamId();
882 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
883 }
884 final int serverCount;
885 try {
886 serverCount = Integer.parseInt(h);
887 } catch (final NumberFormatException e) {
888 resetStreamId();
889 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
890 }
891 final ArrayList<AbstractAcknowledgeableStanza> failedStanzas = new ArrayList<>();
892 final boolean acknowledgedMessages;
893 synchronized (this.mStanzaQueue) {
894 if (serverCount < stanzasSent) {
895 Log.d(
896 Config.LOGTAG,
897 account.getJid().asBareJid() + ": session resumed with lost packages");
898 stanzasSent = serverCount;
899 } else {
900 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": session resumed");
901 }
902 acknowledgedMessages = acknowledgeStanzaUpTo(serverCount);
903 for (int i = 0; i < this.mStanzaQueue.size(); ++i) {
904 failedStanzas.add(mStanzaQueue.valueAt(i));
905 }
906 mStanzaQueue.clear();
907 }
908 if (acknowledgedMessages) {
909 mXmppConnectionService.updateConversationUi();
910 }
911 Log.d(
912 Config.LOGTAG,
913 account.getJid().asBareJid() + ": resending " + failedStanzas.size() + " stanzas");
914 for (final AbstractAcknowledgeableStanza packet : failedStanzas) {
915 if (packet instanceof MessagePacket) {
916 MessagePacket message = (MessagePacket) packet;
917 mXmppConnectionService.markMessage(
918 account,
919 message.getTo().asBareJid(),
920 message.getId(),
921 Message.STATUS_UNSEND);
922 }
923 sendPacket(packet);
924 }
925 changeStatusToOnline();
926 }
927
928 private void changeStatusToOnline() {
929 Log.d(
930 Config.LOGTAG,
931 account.getJid().asBareJid() + ": online with resource " + account.getResource());
932 changeStatus(Account.State.ONLINE);
933 }
934
935 private void processFailed(final Element failed, final boolean sendBindRequest) {
936 final int serverCount;
937 try {
938 serverCount = Integer.parseInt(failed.getAttribute("h"));
939 } catch (final NumberFormatException | NullPointerException e) {
940 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resumption failed");
941 resetStreamId();
942 if (sendBindRequest) {
943 sendBindRequest();
944 }
945 return;
946 }
947 Log.d(
948 Config.LOGTAG,
949 account.getJid().asBareJid()
950 + ": resumption failed but server acknowledged stanza #"
951 + serverCount);
952 final boolean acknowledgedMessages;
953 synchronized (this.mStanzaQueue) {
954 acknowledgedMessages = acknowledgeStanzaUpTo(serverCount);
955 }
956 if (acknowledgedMessages) {
957 mXmppConnectionService.updateConversationUi();
958 }
959 resetStreamId();
960 if (sendBindRequest) {
961 sendBindRequest();
962 }
963 }
964
965 private boolean acknowledgeStanzaUpTo(int serverCount) {
966 if (serverCount > stanzasSent) {
967 Log.e(
968 Config.LOGTAG,
969 "server acknowledged more stanzas than we sent. serverCount="
970 + serverCount
971 + ", ourCount="
972 + stanzasSent);
973 }
974 boolean acknowledgedMessages = false;
975 for (int i = 0; i < mStanzaQueue.size(); ++i) {
976 if (serverCount >= mStanzaQueue.keyAt(i)) {
977 if (Config.EXTENDED_SM_LOGGING) {
978 Log.d(
979 Config.LOGTAG,
980 account.getJid().asBareJid()
981 + ": server acknowledged stanza #"
982 + mStanzaQueue.keyAt(i));
983 }
984 final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i);
985 if (stanza instanceof MessagePacket && acknowledgedListener != null) {
986 final MessagePacket packet = (MessagePacket) stanza;
987 final String id = packet.getId();
988 final Jid to = packet.getTo();
989 if (id != null && to != null) {
990 acknowledgedMessages |=
991 acknowledgedListener.onMessageAcknowledged(account, to, id);
992 }
993 }
994 mStanzaQueue.removeAt(i);
995 i--;
996 }
997 }
998 return acknowledgedMessages;
999 }
1000
1001 private @NonNull Element processPacket(final Tag currentTag, final int packetType)
1002 throws IOException {
1003 final Element element;
1004 switch (packetType) {
1005 case PACKET_IQ:
1006 element = new IqPacket();
1007 break;
1008 case PACKET_MESSAGE:
1009 element = new MessagePacket();
1010 break;
1011 case PACKET_PRESENCE:
1012 element = new PresencePacket();
1013 break;
1014 default:
1015 throw new AssertionError("Should never encounter invalid type");
1016 }
1017 element.setAttributes(currentTag.getAttributes());
1018 Tag nextTag = tagReader.readTag();
1019 if (nextTag == null) {
1020 throw new IOException("interrupted mid tag");
1021 }
1022 while (!nextTag.isEnd(element.getName())) {
1023 if (!nextTag.isNo()) {
1024 element.addChild(tagReader.readElement(nextTag));
1025 }
1026 nextTag = tagReader.readTag();
1027 if (nextTag == null) {
1028 throw new IOException("interrupted mid tag");
1029 }
1030 }
1031 if (stanzasReceived == Integer.MAX_VALUE) {
1032 resetStreamId();
1033 throw new IOException("time to restart the session. cant handle >2 billion pcks");
1034 }
1035 if (inSmacksSession) {
1036 ++stanzasReceived;
1037 } else if (features.sm()) {
1038 Log.d(
1039 Config.LOGTAG,
1040 account.getJid().asBareJid()
1041 + ": not counting stanza("
1042 + element.getClass().getSimpleName()
1043 + "). Not in smacks session.");
1044 }
1045 lastPacketReceived = SystemClock.elapsedRealtime();
1046 if (Config.BACKGROUND_STANZA_LOGGING && mXmppConnectionService.checkListeners()) {
1047 Log.d(Config.LOGTAG, "[background stanza] " + element);
1048 }
1049 if (element instanceof IqPacket
1050 && (((IqPacket) element).getType() == IqPacket.TYPE.SET)
1051 && element.hasChild("jingle", Namespace.JINGLE)) {
1052 return JinglePacket.upgrade((IqPacket) element);
1053 } else {
1054 return element;
1055 }
1056 }
1057
1058 private void processIq(final Tag currentTag) throws IOException {
1059 final IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
1060 if (!packet.valid()) {
1061 Log.e(
1062 Config.LOGTAG,
1063 "encountered invalid iq from='"
1064 + packet.getFrom()
1065 + "' to='"
1066 + packet.getTo()
1067 + "'");
1068 return;
1069 }
1070 if (packet instanceof JinglePacket) {
1071 if (this.jingleListener != null) {
1072 this.jingleListener.onJinglePacketReceived(account, (JinglePacket) packet);
1073 }
1074 } else {
1075 OnIqPacketReceived callback = null;
1076 synchronized (this.packetCallbacks) {
1077 final Pair<IqPacket, OnIqPacketReceived> packetCallbackDuple =
1078 packetCallbacks.get(packet.getId());
1079 if (packetCallbackDuple != null) {
1080 // Packets to the server should have responses from the server
1081 if (packetCallbackDuple.first.toServer(account)) {
1082 if (packet.fromServer(account)) {
1083 callback = packetCallbackDuple.second;
1084 packetCallbacks.remove(packet.getId());
1085 } else {
1086 Log.e(
1087 Config.LOGTAG,
1088 account.getJid().asBareJid().toString()
1089 + ": ignoring spoofed iq packet");
1090 }
1091 } else {
1092 if (packet.getFrom() != null
1093 && packet.getFrom().equals(packetCallbackDuple.first.getTo())) {
1094 callback = packetCallbackDuple.second;
1095 packetCallbacks.remove(packet.getId());
1096 } else {
1097 Log.e(
1098 Config.LOGTAG,
1099 account.getJid().asBareJid().toString()
1100 + ": ignoring spoofed iq packet");
1101 }
1102 }
1103 } else if (packet.getType() == IqPacket.TYPE.GET
1104 || packet.getType() == IqPacket.TYPE.SET) {
1105 callback = this.unregisteredIqListener;
1106 }
1107 }
1108 if (callback != null) {
1109 try {
1110 callback.onIqPacketReceived(account, packet);
1111 } catch (StateChangingError error) {
1112 throw new StateChangingException(error.state);
1113 }
1114 }
1115 }
1116 }
1117
1118 private void processMessage(final Tag currentTag) throws IOException {
1119 final MessagePacket packet = (MessagePacket) processPacket(currentTag, PACKET_MESSAGE);
1120 if (!packet.valid()) {
1121 Log.e(
1122 Config.LOGTAG,
1123 "encountered invalid message from='"
1124 + packet.getFrom()
1125 + "' to='"
1126 + packet.getTo()
1127 + "'");
1128 return;
1129 }
1130 this.messageListener.onMessagePacketReceived(account, packet);
1131 }
1132
1133 private void processPresence(final Tag currentTag) throws IOException {
1134 PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE);
1135 if (!packet.valid()) {
1136 Log.e(
1137 Config.LOGTAG,
1138 "encountered invalid presence from='"
1139 + packet.getFrom()
1140 + "' to='"
1141 + packet.getTo()
1142 + "'");
1143 return;
1144 }
1145 this.presenceListener.onPresencePacketReceived(account, packet);
1146 }
1147
1148 private void sendStartTLS() throws IOException {
1149 final Tag startTLS = Tag.empty("starttls");
1150 startTLS.setAttribute("xmlns", Namespace.TLS);
1151 tagWriter.writeTag(startTLS);
1152 }
1153
1154 private void switchOverToTls() throws XmlPullParserException, IOException {
1155 tagReader.readTag();
1156 final Socket socket = this.socket;
1157 final SSLSocket sslSocket = upgradeSocketToTls(socket);
1158 tagReader.setInputStream(sslSocket.getInputStream());
1159 tagWriter.setOutputStream(sslSocket.getOutputStream());
1160 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS connection established");
1161 final boolean quickStart;
1162 try {
1163 quickStart = establishStream(true);
1164 } catch (final InterruptedException e) {
1165 return;
1166 }
1167 if (quickStart) {
1168 this.quickStartInProgress = true;
1169 }
1170 features.encryptionEnabled = true;
1171 final Tag tag = tagReader.readTag();
1172 if (tag != null && tag.isStart("stream", Namespace.STREAMS)) {
1173 SSLSocketHelper.log(account, sslSocket);
1174 processStream();
1175 } else {
1176 throw new StateChangingException(Account.State.STREAM_OPENING_ERROR);
1177 }
1178 sslSocket.close();
1179 }
1180
1181 private SSLSocket upgradeSocketToTls(final Socket socket) throws IOException {
1182 final SSLSocketFactory sslSocketFactory;
1183 try {
1184 sslSocketFactory = getSSLSocketFactory();
1185 } catch (final NoSuchAlgorithmException | KeyManagementException e) {
1186 throw new StateChangingException(Account.State.TLS_ERROR);
1187 }
1188 final InetAddress address = socket.getInetAddress();
1189 final SSLSocket sslSocket =
1190 (SSLSocket)
1191 sslSocketFactory.createSocket(
1192 socket, address.getHostAddress(), socket.getPort(), true);
1193 SSLSocketHelper.setSecurity(sslSocket);
1194 SSLSocketHelper.setHostname(sslSocket, IDN.toASCII(account.getServer()));
1195 SSLSocketHelper.setApplicationProtocol(sslSocket, "xmpp-client");
1196 final XmppDomainVerifier xmppDomainVerifier = new XmppDomainVerifier();
1197 try {
1198 if (!xmppDomainVerifier.verify(
1199 account.getServer(), this.verifiedHostname, sslSocket.getSession())) {
1200 Log.d(
1201 Config.LOGTAG,
1202 account.getJid().asBareJid()
1203 + ": TLS certificate domain verification failed");
1204 FileBackend.close(sslSocket);
1205 throw new StateChangingException(Account.State.TLS_ERROR_DOMAIN);
1206 }
1207 } catch (final SSLPeerUnverifiedException e) {
1208 FileBackend.close(sslSocket);
1209 throw new StateChangingException(Account.State.TLS_ERROR);
1210 }
1211 return sslSocket;
1212 }
1213
1214 private void processStreamFeatures(final Tag currentTag) throws IOException {
1215 this.streamFeatures = tagReader.readElement(currentTag);
1216 final boolean isSecure =
1217 features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion();
1218 final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER);
1219 if (this.quickStartInProgress) {
1220 if (this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) {
1221 Log.d(
1222 Config.LOGTAG,
1223 account.getJid().asBareJid()
1224 + ": quick start in progress. ignoring features: "
1225 + XmlHelper.printElementNames(this.streamFeatures));
1226 return;
1227 }
1228 Log.d(Config.LOGTAG,account.getJid().asBareJid()+": server lost support for SASL 2. quick start not possible");
1229 this.account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, false);
1230 mXmppConnectionService.updateAccount(account);
1231 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1232 }
1233 if (this.streamFeatures.hasChild("starttls", Namespace.TLS)
1234 && !features.encryptionEnabled) {
1235 sendStartTLS();
1236 } else if (this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE)
1237 && account.isOptionSet(Account.OPTION_REGISTER)) {
1238 if (isSecure) {
1239 register();
1240 } else {
1241 Log.d(
1242 Config.LOGTAG,
1243 account.getJid().asBareJid()
1244 + ": unable to find STARTTLS for registration process "
1245 + XmlHelper.printElementNames(this.streamFeatures));
1246 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1247 }
1248 } else if (!this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE)
1249 && account.isOptionSet(Account.OPTION_REGISTER)) {
1250 throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED);
1251 } else if (Config.SASL_2_ENABLED
1252 && this.streamFeatures.hasChild("authentication", Namespace.SASL_2)
1253 && shouldAuthenticate
1254 && isSecure) {
1255 authenticate(SaslMechanism.Version.SASL_2);
1256 } else if (this.streamFeatures.hasChild("mechanisms", Namespace.SASL)
1257 && shouldAuthenticate
1258 && isSecure) {
1259 authenticate(SaslMechanism.Version.SASL);
1260 } else if (this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT)
1261 && streamId != null
1262 && !inSmacksSession) {
1263 if (Config.EXTENDED_SM_LOGGING) {
1264 Log.d(
1265 Config.LOGTAG,
1266 account.getJid().asBareJid()
1267 + ": resuming after stanza #"
1268 + stanzasReceived);
1269 }
1270 final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived);
1271 this.mSmCatchupMessageCounter.set(0);
1272 this.mWaitingForSmCatchup.set(true);
1273 this.tagWriter.writeStanzaAsync(resume);
1274 } else if (needsBinding) {
1275 if (this.streamFeatures.hasChild("bind", Namespace.BIND) && isSecure) {
1276 sendBindRequest();
1277 } else {
1278 Log.d(
1279 Config.LOGTAG,
1280 account.getJid().asBareJid()
1281 + ": unable to find bind feature "
1282 + XmlHelper.printElementNames(this.streamFeatures));
1283 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1284 }
1285 } else {
1286 Log.d(
1287 Config.LOGTAG,
1288 account.getJid().asBareJid()
1289 + ": received NOP stream features "
1290 + XmlHelper.printElementNames(this.streamFeatures));
1291 }
1292 }
1293
1294 private void authenticate(final SaslMechanism.Version version) throws IOException {
1295 final Element authElement;
1296 if (version == SaslMechanism.Version.SASL) {
1297 authElement = this.streamFeatures.findChild("mechanisms", Namespace.SASL);
1298 } else {
1299 authElement = this.streamFeatures.findChild("authentication", Namespace.SASL_2);
1300 }
1301 //TODO externalize
1302 final Collection<String> mechanisms =
1303 Collections2.transform(
1304 Collections2.filter(
1305 authElement.getChildren(),
1306 c -> c != null && "mechanism".equals(c.getName())),
1307 c -> c == null ? null : c.getContent());
1308 final Element cbElement =
1309 this.streamFeatures.findChild("sasl-channel-binding", Namespace.CHANNEL_BINDING);
1310 final Collection<ChannelBinding> channelBindings =
1311 Collections2.filter(
1312 Collections2.transform(
1313 Collections2.filter(
1314 cbElement == null
1315 ? Collections.emptyList()
1316 : cbElement.getChildren(),
1317 c -> c != null && "channel-binding".equals(c.getName())),
1318 c -> c == null ? null : ChannelBinding.of(c.getAttribute("type"))),
1319 Predicates.notNull());
1320 Log.d(Config.LOGTAG,"mechanisms: "+mechanisms);
1321 Log.d(Config.LOGTAG, "channel bindings: " + channelBindings);
1322 final SaslMechanism.Factory factory = new SaslMechanism.Factory(account);
1323 this.saslMechanism = factory.of(mechanisms, channelBindings);
1324
1325 //TODO externalize checks
1326
1327 if (saslMechanism == null) {
1328 Log.d(
1329 Config.LOGTAG,
1330 account.getJid().asBareJid()
1331 + ": unable to find supported SASL mechanism in "
1332 + mechanisms);
1333 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1334 }
1335 final int pinnedMechanism = account.getPinnedMechanismPriority();
1336 if (pinnedMechanism > saslMechanism.getPriority()) {
1337 Log.e(
1338 Config.LOGTAG,
1339 "Auth failed. Authentication mechanism "
1340 + saslMechanism.getMechanism()
1341 + " has lower priority ("
1342 + saslMechanism.getPriority()
1343 + ") than pinned priority ("
1344 + pinnedMechanism
1345 + "). Possible downgrade attack?");
1346 throw new StateChangingException(Account.State.DOWNGRADE_ATTACK);
1347 }
1348 final boolean quickStartAvailable;
1349 final String firstMessage = saslMechanism.getClientFirstMessage();
1350 final Element authenticate;
1351 if (version == SaslMechanism.Version.SASL) {
1352 authenticate = new Element("auth", Namespace.SASL);
1353 if (!Strings.isNullOrEmpty(firstMessage)) {
1354 authenticate.setContent(firstMessage);
1355 }
1356 quickStartAvailable = false;
1357 } else if (version == SaslMechanism.Version.SASL_2) {
1358 final Element inline = authElement.findChild("inline", Namespace.SASL_2);
1359 final boolean sm = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3");
1360 final Collection<String> bindFeatures = Bind2.features(inline);
1361 quickStartAvailable =
1362 sm
1363 && bindFeatures != null
1364 && bindFeatures.containsAll(Bind2.QUICKSTART_FEATURES);
1365 if (bindFeatures != null) {
1366 try {
1367 mXmppConnectionService.restoredFromDatabaseLatch.await();
1368 } catch (final InterruptedException e) {
1369 Log.d(
1370 Config.LOGTAG,
1371 account.getJid().asBareJid()
1372 + ": interrupted while waiting for DB restore during SASL2 bind");
1373 return;
1374 }
1375 }
1376 authenticate = generateAuthenticationRequest(firstMessage, bindFeatures, sm);
1377 } else {
1378 throw new AssertionError("Missing implementation for " + version);
1379 }
1380
1381 if (account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, quickStartAvailable)) {
1382 mXmppConnectionService.updateAccount(account);
1383 }
1384
1385 Log.d(
1386 Config.LOGTAG,
1387 account.getJid().toString()
1388 + ": Authenticating with "
1389 + version
1390 + "/"
1391 + saslMechanism.getMechanism());
1392 authenticate.setAttribute("mechanism", saslMechanism.getMechanism());
1393 tagWriter.writeElement(authenticate);
1394 }
1395
1396 private Element generateAuthenticationRequest(final String firstMessage) {
1397 return generateAuthenticationRequest(firstMessage, Bind2.QUICKSTART_FEATURES, true);
1398 }
1399
1400 private Element generateAuthenticationRequest(
1401 final String firstMessage,
1402 final Collection<String> bind,
1403 final boolean inlineStreamManagement) {
1404 final Element authenticate = new Element("authenticate", Namespace.SASL_2);
1405 if (!Strings.isNullOrEmpty(firstMessage)) {
1406 authenticate.addChild("initial-response").setContent(firstMessage);
1407 }
1408 final Element userAgent = authenticate.addChild("user-agent");
1409 userAgent.setAttribute("id", account.getUuid());
1410 userAgent
1411 .addChild("software")
1412 .setContent(mXmppConnectionService.getString(R.string.app_name));
1413 if (!PhoneHelper.isEmulator()) {
1414 userAgent
1415 .addChild("device")
1416 .setContent(String.format("%s %s", Build.MANUFACTURER, Build.MODEL));
1417 }
1418 if (bind != null) {
1419 authenticate.addChild(generateBindRequest(bind));
1420 }
1421 if (inlineStreamManagement && streamId != null) {
1422 final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived);
1423 this.mSmCatchupMessageCounter.set(0);
1424 this.mWaitingForSmCatchup.set(true);
1425 authenticate.addChild(resume);
1426 }
1427 return authenticate;
1428 }
1429
1430 private Element generateBindRequest(final Collection<String> bindFeatures) {
1431 Log.d(Config.LOGTAG, "inline bind features: " + bindFeatures);
1432 final Element bind = new Element("bind", Namespace.BIND2);
1433 bind.addChild("tag").setContent(mXmppConnectionService.getString(R.string.app_name));
1434 final Element features = bind.addChild("features");
1435 if (bindFeatures.contains(Namespace.CARBONS)) {
1436 features.addChild("enable", Namespace.CARBONS);
1437 }
1438 if (bindFeatures.contains(Namespace.STREAM_MANAGEMENT)) {
1439 features.addChild(new EnablePacket());
1440 }
1441 return bind;
1442 }
1443
1444 private void register() {
1445 final String preAuth = account.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN);
1446 if (preAuth != null && features.invite()) {
1447 final IqPacket preAuthRequest = new IqPacket(IqPacket.TYPE.SET);
1448 preAuthRequest.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth);
1449 sendUnmodifiedIqPacket(
1450 preAuthRequest,
1451 (account, response) -> {
1452 if (response.getType() == IqPacket.TYPE.RESULT) {
1453 sendRegistryRequest();
1454 } else {
1455 final String error = response.getErrorCondition();
1456 Log.d(
1457 Config.LOGTAG,
1458 account.getJid().asBareJid()
1459 + ": failed to pre auth. "
1460 + error);
1461 throw new StateChangingError(Account.State.REGISTRATION_INVALID_TOKEN);
1462 }
1463 },
1464 true);
1465 } else {
1466 sendRegistryRequest();
1467 }
1468 }
1469
1470 private void sendRegistryRequest() {
1471 final IqPacket register = new IqPacket(IqPacket.TYPE.GET);
1472 register.query(Namespace.REGISTER);
1473 register.setTo(account.getDomain());
1474 sendUnmodifiedIqPacket(
1475 register,
1476 (account, packet) -> {
1477 if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
1478 return;
1479 }
1480 if (packet.getType() == IqPacket.TYPE.ERROR) {
1481 throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1482 }
1483 final Element query = packet.query(Namespace.REGISTER);
1484 if (query.hasChild("username") && (query.hasChild("password"))) {
1485 final IqPacket register1 = new IqPacket(IqPacket.TYPE.SET);
1486 final Element username =
1487 new Element("username").setContent(account.getUsername());
1488 final Element password =
1489 new Element("password").setContent(account.getPassword());
1490 register1.query(Namespace.REGISTER).addChild(username);
1491 register1.query().addChild(password);
1492 register1.setFrom(account.getJid().asBareJid());
1493 sendUnmodifiedIqPacket(register1, registrationResponseListener, true);
1494 } else if (query.hasChild("x", Namespace.DATA)) {
1495 final Data data = Data.parse(query.findChild("x", Namespace.DATA));
1496 final Element blob = query.findChild("data", "urn:xmpp:bob");
1497 final String id = packet.getId();
1498 InputStream is;
1499 if (blob != null) {
1500 try {
1501 final String base64Blob = blob.getContent();
1502 final byte[] strBlob = Base64.decode(base64Blob, Base64.DEFAULT);
1503 is = new ByteArrayInputStream(strBlob);
1504 } catch (Exception e) {
1505 is = null;
1506 }
1507 } else {
1508 final boolean useTor =
1509 mXmppConnectionService.useTorToConnect() || account.isOnion();
1510 try {
1511 final String url = data.getValue("url");
1512 final String fallbackUrl = data.getValue("captcha-fallback-url");
1513 if (url != null) {
1514 is = HttpConnectionManager.open(url, useTor);
1515 } else if (fallbackUrl != null) {
1516 is = HttpConnectionManager.open(fallbackUrl, useTor);
1517 } else {
1518 is = null;
1519 }
1520 } catch (final IOException e) {
1521 Log.d(
1522 Config.LOGTAG,
1523 account.getJid().asBareJid() + ": unable to fetch captcha",
1524 e);
1525 is = null;
1526 }
1527 }
1528
1529 if (is != null) {
1530 Bitmap captcha = BitmapFactory.decodeStream(is);
1531 try {
1532 if (mXmppConnectionService.displayCaptchaRequest(
1533 account, id, data, captcha)) {
1534 return;
1535 }
1536 } catch (Exception e) {
1537 throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1538 }
1539 }
1540 throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1541 } else if (query.hasChild("instructions")
1542 || query.hasChild("x", Namespace.OOB)) {
1543 final String instructions = query.findChildContent("instructions");
1544 final Element oob = query.findChild("x", Namespace.OOB);
1545 final String url = oob == null ? null : oob.findChildContent("url");
1546 if (url != null) {
1547 setAccountCreationFailed(url);
1548 } else if (instructions != null) {
1549 final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(instructions);
1550 if (matcher.find()) {
1551 setAccountCreationFailed(
1552 instructions.substring(matcher.start(), matcher.end()));
1553 }
1554 }
1555 throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1556 }
1557 },
1558 true);
1559 }
1560
1561 private void setAccountCreationFailed(final String url) {
1562 final HttpUrl httpUrl = url == null ? null : HttpUrl.parse(url);
1563 if (httpUrl != null && httpUrl.isHttps()) {
1564 this.redirectionUrl = httpUrl;
1565 throw new StateChangingError(Account.State.REGISTRATION_WEB);
1566 }
1567 throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1568 }
1569
1570 public HttpUrl getRedirectionUrl() {
1571 return this.redirectionUrl;
1572 }
1573
1574 public void resetEverything() {
1575 resetAttemptCount(true);
1576 resetStreamId();
1577 clearIqCallbacks();
1578 this.stanzasSent = 0;
1579 mStanzaQueue.clear();
1580 this.redirectionUrl = null;
1581 synchronized (this.disco) {
1582 disco.clear();
1583 }
1584 synchronized (this.commands) {
1585 this.commands.clear();
1586 }
1587 }
1588
1589 private void sendBindRequest() {
1590 try {
1591 mXmppConnectionService.restoredFromDatabaseLatch.await();
1592 } catch (InterruptedException e) {
1593 Log.d(
1594 Config.LOGTAG,
1595 account.getJid().asBareJid()
1596 + ": interrupted while waiting for DB restore during bind");
1597 return;
1598 }
1599 clearIqCallbacks();
1600 if (account.getJid().isBareJid()) {
1601 account.setResource(this.createNewResource());
1602 } else {
1603 fixResource(mXmppConnectionService, account);
1604 }
1605 final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
1606 final String resource =
1607 Config.USE_RANDOM_RESOURCE_ON_EVERY_BIND ? nextRandomId() : account.getResource();
1608 iq.addChild("bind", Namespace.BIND).addChild("resource").setContent(resource);
1609 this.sendUnmodifiedIqPacket(
1610 iq,
1611 (account, packet) -> {
1612 if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
1613 return;
1614 }
1615 final Element bind = packet.findChild("bind");
1616 if (bind != null && packet.getType() == IqPacket.TYPE.RESULT) {
1617 isBound = true;
1618 final Element jid = bind.findChild("jid");
1619 if (jid != null && jid.getContent() != null) {
1620 try {
1621 Jid assignedJid = Jid.ofEscaped(jid.getContent());
1622 if (!account.getJid().getDomain().equals(assignedJid.getDomain())) {
1623 Log.d(
1624 Config.LOGTAG,
1625 account.getJid().asBareJid()
1626 + ": server tried to re-assign domain to "
1627 + assignedJid.getDomain());
1628 throw new StateChangingError(Account.State.BIND_FAILURE);
1629 }
1630 if (account.setJid(assignedJid)) {
1631 Log.d(
1632 Config.LOGTAG,
1633 account.getJid().asBareJid()
1634 + ": jid changed during bind. updating database");
1635 mXmppConnectionService.databaseBackend.updateAccount(account);
1636 }
1637 if (streamFeatures.hasChild("session")
1638 && !streamFeatures
1639 .findChild("session")
1640 .hasChild("optional")) {
1641 sendStartSession();
1642 } else {
1643 final boolean waitForDisco = enableStreamManagement();
1644 sendPostBindInitialization(waitForDisco, false);
1645 }
1646 return;
1647 } catch (final IllegalArgumentException e) {
1648 Log.d(
1649 Config.LOGTAG,
1650 account.getJid().asBareJid()
1651 + ": server reported invalid jid ("
1652 + jid.getContent()
1653 + ") on bind");
1654 }
1655 } else {
1656 Log.d(
1657 Config.LOGTAG,
1658 account.getJid()
1659 + ": disconnecting because of bind failure. (no jid)");
1660 }
1661 } else {
1662 Log.d(
1663 Config.LOGTAG,
1664 account.getJid()
1665 + ": disconnecting because of bind failure ("
1666 + packet);
1667 }
1668 final Element error = packet.findChild("error");
1669 if (packet.getType() == IqPacket.TYPE.ERROR
1670 && error != null
1671 && error.hasChild("conflict")) {
1672 account.setResource(createNewResource());
1673 }
1674 throw new StateChangingError(Account.State.BIND_FAILURE);
1675 },
1676 true);
1677 }
1678
1679 private void clearIqCallbacks() {
1680 final IqPacket failurePacket = new IqPacket(IqPacket.TYPE.TIMEOUT);
1681 final ArrayList<OnIqPacketReceived> callbacks = new ArrayList<>();
1682 synchronized (this.packetCallbacks) {
1683 if (this.packetCallbacks.size() == 0) {
1684 return;
1685 }
1686 Log.d(
1687 Config.LOGTAG,
1688 account.getJid().asBareJid()
1689 + ": clearing "
1690 + this.packetCallbacks.size()
1691 + " iq callbacks");
1692 final Iterator<Pair<IqPacket, OnIqPacketReceived>> iterator =
1693 this.packetCallbacks.values().iterator();
1694 while (iterator.hasNext()) {
1695 Pair<IqPacket, OnIqPacketReceived> entry = iterator.next();
1696 callbacks.add(entry.second);
1697 iterator.remove();
1698 }
1699 }
1700 for (OnIqPacketReceived callback : callbacks) {
1701 try {
1702 callback.onIqPacketReceived(account, failurePacket);
1703 } catch (StateChangingError error) {
1704 Log.d(
1705 Config.LOGTAG,
1706 account.getJid().asBareJid()
1707 + ": caught StateChangingError("
1708 + error.state.toString()
1709 + ") while clearing callbacks");
1710 // ignore
1711 }
1712 }
1713 Log.d(
1714 Config.LOGTAG,
1715 account.getJid().asBareJid()
1716 + ": done clearing iq callbacks. "
1717 + this.packetCallbacks.size()
1718 + " left");
1719 }
1720
1721 public void sendDiscoTimeout() {
1722 if (mWaitForDisco.compareAndSet(true, false)) {
1723 Log.d(
1724 Config.LOGTAG,
1725 account.getJid().asBareJid() + ": finalizing bind after disco timeout");
1726 finalizeBind();
1727 }
1728 }
1729
1730 private void sendStartSession() {
1731 Log.d(
1732 Config.LOGTAG,
1733 account.getJid().asBareJid() + ": sending legacy session to outdated server");
1734 final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET);
1735 startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session");
1736 this.sendUnmodifiedIqPacket(
1737 startSession,
1738 (account, packet) -> {
1739 if (packet.getType() == IqPacket.TYPE.RESULT) {
1740 final boolean waitForDisco = enableStreamManagement();
1741 sendPostBindInitialization(waitForDisco, false);
1742 } else if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
1743 throw new StateChangingError(Account.State.SESSION_FAILURE);
1744 }
1745 },
1746 true);
1747 }
1748
1749 private boolean enableStreamManagement() {
1750 final boolean streamManagement =
1751 this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT);
1752 if (streamManagement) {
1753 synchronized (this.mStanzaQueue) {
1754 final EnablePacket enable = new EnablePacket();
1755 tagWriter.writeStanzaAsync(enable);
1756 stanzasSent = 0;
1757 mStanzaQueue.clear();
1758 }
1759 return true;
1760 } else {
1761 return false;
1762 }
1763 }
1764
1765 private void sendPostBindInitialization(
1766 final boolean waitForDisco, final boolean carbonsEnabled) {
1767 features.carbonsEnabled = carbonsEnabled;
1768 features.blockListRequested = false;
1769 synchronized (this.disco) {
1770 this.disco.clear();
1771 }
1772 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery");
1773 mPendingServiceDiscoveries.set(0);
1774 if (!waitForDisco
1775 || Patches.DISCO_EXCEPTIONS.contains(
1776 account.getJid().getDomain().toEscapedString())) {
1777 Log.d(
1778 Config.LOGTAG,
1779 account.getJid().asBareJid() + ": do not wait for service discovery");
1780 mWaitForDisco.set(false);
1781 } else {
1782 mWaitForDisco.set(true);
1783 }
1784 lastDiscoStarted = SystemClock.elapsedRealtime();
1785 mXmppConnectionService.scheduleWakeUpCall(
1786 Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
1787 final Element caps = streamFeatures.findChild("c");
1788 final String hash = caps == null ? null : caps.getAttribute("hash");
1789 final String ver = caps == null ? null : caps.getAttribute("ver");
1790 ServiceDiscoveryResult discoveryResult = null;
1791 if (hash != null && ver != null) {
1792 discoveryResult =
1793 mXmppConnectionService.getCachedServiceDiscoveryResult(new Pair<>(hash, ver));
1794 }
1795 final boolean requestDiscoItemsFirst =
1796 !account.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY);
1797 if (requestDiscoItemsFirst) {
1798 sendServiceDiscoveryItems(account.getDomain());
1799 }
1800 if (discoveryResult == null) {
1801 sendServiceDiscoveryInfo(account.getDomain());
1802 } else {
1803 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server caps came from cache");
1804 disco.put(account.getDomain(), discoveryResult);
1805 }
1806 discoverMamPreferences();
1807 sendServiceDiscoveryInfo(account.getJid().asBareJid());
1808 if (!requestDiscoItemsFirst) {
1809 sendServiceDiscoveryItems(account.getDomain());
1810 }
1811
1812 if (!mWaitForDisco.get()) {
1813 finalizeBind();
1814 }
1815 this.lastSessionStarted = SystemClock.elapsedRealtime();
1816 }
1817
1818 private void sendServiceDiscoveryInfo(final Jid jid) {
1819 mPendingServiceDiscoveries.incrementAndGet();
1820 final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
1821 iq.setTo(jid);
1822 iq.query("http://jabber.org/protocol/disco#info");
1823 this.sendIqPacket(
1824 iq,
1825 (account, packet) -> {
1826 if (packet.getType() == IqPacket.TYPE.RESULT) {
1827 boolean advancedStreamFeaturesLoaded;
1828 synchronized (XmppConnection.this.disco) {
1829 ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet);
1830 if (jid.equals(account.getDomain())) {
1831 mXmppConnectionService.databaseBackend.insertDiscoveryResult(
1832 result);
1833 }
1834 disco.put(jid, result);
1835 advancedStreamFeaturesLoaded =
1836 disco.containsKey(account.getDomain())
1837 && disco.containsKey(account.getJid().asBareJid());
1838 }
1839 if (advancedStreamFeaturesLoaded
1840 && (jid.equals(account.getDomain())
1841 || jid.equals(account.getJid().asBareJid()))) {
1842 enableAdvancedStreamFeatures();
1843 }
1844 } else if (packet.getType() == IqPacket.TYPE.ERROR) {
1845 Log.d(
1846 Config.LOGTAG,
1847 account.getJid().asBareJid()
1848 + ": could not query disco info for "
1849 + jid.toString());
1850 final boolean serverOrAccount =
1851 jid.equals(account.getDomain())
1852 || jid.equals(account.getJid().asBareJid());
1853 final boolean advancedStreamFeaturesLoaded;
1854 if (serverOrAccount) {
1855 synchronized (XmppConnection.this.disco) {
1856 disco.put(jid, ServiceDiscoveryResult.empty());
1857 advancedStreamFeaturesLoaded =
1858 disco.containsKey(account.getDomain())
1859 && disco.containsKey(account.getJid().asBareJid());
1860 }
1861 } else {
1862 advancedStreamFeaturesLoaded = false;
1863 }
1864 if (advancedStreamFeaturesLoaded) {
1865 enableAdvancedStreamFeatures();
1866 }
1867 }
1868 if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
1869 if (mPendingServiceDiscoveries.decrementAndGet() == 0
1870 && mWaitForDisco.compareAndSet(true, false)) {
1871 finalizeBind();
1872 }
1873 }
1874 });
1875 }
1876
1877 private void discoverMamPreferences() {
1878 IqPacket request = new IqPacket(IqPacket.TYPE.GET);
1879 request.addChild("prefs", MessageArchiveService.Version.MAM_2.namespace);
1880 sendIqPacket(
1881 request,
1882 (account, response) -> {
1883 if (response.getType() == IqPacket.TYPE.RESULT) {
1884 Element prefs =
1885 response.findChild(
1886 "prefs", MessageArchiveService.Version.MAM_2.namespace);
1887 isMamPreferenceAlways =
1888 "always"
1889 .equals(
1890 prefs == null
1891 ? null
1892 : prefs.getAttribute("default"));
1893 }
1894 });
1895 }
1896
1897 private void discoverCommands() {
1898 final IqPacket request = new IqPacket(IqPacket.TYPE.GET);
1899 request.setTo(account.getDomain());
1900 request.addChild("query", Namespace.DISCO_ITEMS).setAttribute("node", Namespace.COMMANDS);
1901 sendIqPacket(
1902 request,
1903 (account, response) -> {
1904 if (response.getType() == IqPacket.TYPE.RESULT) {
1905 final Element query = response.findChild("query", Namespace.DISCO_ITEMS);
1906 if (query == null) {
1907 return;
1908 }
1909 final HashMap<String, Jid> commands = new HashMap<>();
1910 for (final Element child : query.getChildren()) {
1911 if ("item".equals(child.getName())) {
1912 final String node = child.getAttribute("node");
1913 final Jid jid = child.getAttributeAsJid("jid");
1914 if (node != null && jid != null) {
1915 commands.put(node, jid);
1916 }
1917 }
1918 }
1919 synchronized (this.commands) {
1920 this.commands.clear();
1921 this.commands.putAll(commands);
1922 }
1923 }
1924 });
1925 }
1926
1927 public boolean isMamPreferenceAlways() {
1928 return isMamPreferenceAlways;
1929 }
1930
1931 private void finalizeBind() {
1932 if (bindListener != null) {
1933 bindListener.onBind(account);
1934 }
1935 changeStatusToOnline();
1936 }
1937
1938 private void enableAdvancedStreamFeatures() {
1939 if (getFeatures().blocking() && !features.blockListRequested) {
1940 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Requesting block list");
1941 this.sendIqPacket(
1942 getIqGenerator().generateGetBlockList(), mXmppConnectionService.getIqParser());
1943 }
1944 for (final OnAdvancedStreamFeaturesLoaded listener :
1945 advancedStreamFeaturesLoadedListeners) {
1946 listener.onAdvancedStreamFeaturesAvailable(account);
1947 }
1948 if (getFeatures().carbons() && !features.carbonsEnabled) {
1949 sendEnableCarbons();
1950 }
1951 if (getFeatures().commands()) {
1952 discoverCommands();
1953 }
1954 }
1955
1956 private void sendServiceDiscoveryItems(final Jid server) {
1957 mPendingServiceDiscoveries.incrementAndGet();
1958 final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
1959 iq.setTo(server.getDomain());
1960 iq.query("http://jabber.org/protocol/disco#items");
1961 this.sendIqPacket(
1962 iq,
1963 (account, packet) -> {
1964 if (packet.getType() == IqPacket.TYPE.RESULT) {
1965 final HashSet<Jid> items = new HashSet<>();
1966 final List<Element> elements = packet.query().getChildren();
1967 for (final Element element : elements) {
1968 if (element.getName().equals("item")) {
1969 final Jid jid =
1970 InvalidJid.getNullForInvalid(
1971 element.getAttributeAsJid("jid"));
1972 if (jid != null && !jid.equals(account.getDomain())) {
1973 items.add(jid);
1974 }
1975 }
1976 }
1977 for (Jid jid : items) {
1978 sendServiceDiscoveryInfo(jid);
1979 }
1980 } else {
1981 Log.d(
1982 Config.LOGTAG,
1983 account.getJid().asBareJid()
1984 + ": could not query disco items of "
1985 + server);
1986 }
1987 if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
1988 if (mPendingServiceDiscoveries.decrementAndGet() == 0
1989 && mWaitForDisco.compareAndSet(true, false)) {
1990 finalizeBind();
1991 }
1992 }
1993 });
1994 }
1995
1996 private void sendEnableCarbons() {
1997 final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
1998 iq.addChild("enable", Namespace.CARBONS);
1999 this.sendIqPacket(
2000 iq,
2001 (account, packet) -> {
2002 if (packet.getType() == IqPacket.TYPE.RESULT) {
2003 Log.d(
2004 Config.LOGTAG,
2005 account.getJid().asBareJid() + ": successfully enabled carbons");
2006 features.carbonsEnabled = true;
2007 } else {
2008 Log.d(
2009 Config.LOGTAG,
2010 account.getJid().asBareJid()
2011 + ": could not enable carbons "
2012 + packet);
2013 }
2014 });
2015 }
2016
2017 private void processStreamError(final Tag currentTag) throws IOException {
2018 final Element streamError = tagReader.readElement(currentTag);
2019 if (streamError == null) {
2020 return;
2021 }
2022 if (streamError.hasChild("conflict")) {
2023 account.setResource(createNewResource());
2024 Log.d(
2025 Config.LOGTAG,
2026 account.getJid().asBareJid()
2027 + ": switching resource due to conflict ("
2028 + account.getResource()
2029 + ")");
2030 throw new IOException();
2031 } else if (streamError.hasChild("host-unknown")) {
2032 throw new StateChangingException(Account.State.HOST_UNKNOWN);
2033 } else if (streamError.hasChild("policy-violation")) {
2034 this.lastConnect = SystemClock.elapsedRealtime();
2035 final String text = streamError.findChildContent("text");
2036 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": policy violation. " + text);
2037 failPendingMessages(text);
2038 throw new StateChangingException(Account.State.POLICY_VIOLATION);
2039 } else {
2040 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError);
2041 throw new StateChangingException(Account.State.STREAM_ERROR);
2042 }
2043 }
2044
2045 private void failPendingMessages(final String error) {
2046 synchronized (this.mStanzaQueue) {
2047 for (int i = 0; i < mStanzaQueue.size(); ++i) {
2048 final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i);
2049 if (stanza instanceof MessagePacket) {
2050 final MessagePacket packet = (MessagePacket) stanza;
2051 final String id = packet.getId();
2052 final Jid to = packet.getTo();
2053 mXmppConnectionService.markMessage(
2054 account, to.asBareJid(), id, Message.STATUS_SEND_FAILED, error);
2055 }
2056 }
2057 }
2058 }
2059
2060 private boolean establishStream(final boolean secureConnection) throws IOException, InterruptedException {
2061 final SaslMechanism saslMechanism = account.getPinnedMechanism();
2062 if (secureConnection
2063 && Config.SASL_2_ENABLED
2064 && saslMechanism != null
2065 && account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) {
2066 mXmppConnectionService.restoredFromDatabaseLatch.await();
2067 this.saslMechanism = saslMechanism;
2068 final Element authenticate =
2069 generateAuthenticationRequest(saslMechanism.getClientFirstMessage());
2070 authenticate.setAttribute("mechanism", saslMechanism.getMechanism());
2071 sendStartStream(false);
2072 tagWriter.writeElement(authenticate);
2073 Log.d(
2074 Config.LOGTAG,
2075 account.getJid().toString()
2076 + ": quick start with "
2077 + saslMechanism.getMechanism());
2078 return true;
2079 } else {
2080 sendStartStream(true);
2081 return false;
2082 }
2083 }
2084
2085 private void sendStartStream(final boolean flush) throws IOException {
2086 final Tag stream = Tag.start("stream:stream");
2087 stream.setAttribute("to", account.getServer());
2088 stream.setAttribute("version", "1.0");
2089 stream.setAttribute("xml:lang", LocalizedContent.STREAM_LANGUAGE);
2090 stream.setAttribute("xmlns", "jabber:client");
2091 stream.setAttribute("xmlns:stream", Namespace.STREAMS);
2092 tagWriter.writeTag(stream, flush);
2093 }
2094
2095 private String createNewResource() {
2096 return mXmppConnectionService.getString(R.string.app_name) + '.' + nextRandomId(true);
2097 }
2098
2099 private String nextRandomId() {
2100 return nextRandomId(false);
2101 }
2102
2103 private String nextRandomId(final boolean s) {
2104 return CryptoHelper.random(s ? 3 : 9);
2105 }
2106
2107 public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) {
2108 packet.setFrom(account.getJid());
2109 return this.sendUnmodifiedIqPacket(packet, callback, false);
2110 }
2111
2112 public synchronized String sendUnmodifiedIqPacket(
2113 final IqPacket packet, final OnIqPacketReceived callback, boolean force) {
2114 if (packet.getId() == null) {
2115 packet.setAttribute("id", nextRandomId());
2116 }
2117 if (callback != null) {
2118 synchronized (this.packetCallbacks) {
2119 packetCallbacks.put(packet.getId(), new Pair<>(packet, callback));
2120 }
2121 }
2122 this.sendPacket(packet, force);
2123 return packet.getId();
2124 }
2125
2126 public void sendMessagePacket(final MessagePacket packet) {
2127 this.sendPacket(packet);
2128 }
2129
2130 public void sendPresencePacket(final PresencePacket packet) {
2131 this.sendPacket(packet);
2132 }
2133
2134 private synchronized void sendPacket(final AbstractStanza packet) {
2135 sendPacket(packet, false);
2136 }
2137
2138 private synchronized void sendPacket(final AbstractStanza packet, final boolean force) {
2139 if (stanzasSent == Integer.MAX_VALUE) {
2140 resetStreamId();
2141 disconnect(true);
2142 return;
2143 }
2144 synchronized (this.mStanzaQueue) {
2145 if (force || isBound) {
2146 tagWriter.writeStanzaAsync(packet);
2147 } else {
2148 Log.d(
2149 Config.LOGTAG,
2150 account.getJid().asBareJid()
2151 + " do not write stanza to unbound stream "
2152 + packet.toString());
2153 }
2154 if (packet instanceof AbstractAcknowledgeableStanza) {
2155 AbstractAcknowledgeableStanza stanza = (AbstractAcknowledgeableStanza) packet;
2156
2157 if (this.mStanzaQueue.size() != 0) {
2158 int currentHighestKey = this.mStanzaQueue.keyAt(this.mStanzaQueue.size() - 1);
2159 if (currentHighestKey != stanzasSent) {
2160 throw new AssertionError("Stanza count messed up");
2161 }
2162 }
2163
2164 ++stanzasSent;
2165 this.mStanzaQueue.append(stanzasSent, stanza);
2166 if (stanza instanceof MessagePacket && stanza.getId() != null && inSmacksSession) {
2167 if (Config.EXTENDED_SM_LOGGING) {
2168 Log.d(
2169 Config.LOGTAG,
2170 account.getJid().asBareJid()
2171 + ": requesting ack for message stanza #"
2172 + stanzasSent);
2173 }
2174 tagWriter.writeStanzaAsync(new RequestPacket());
2175 }
2176 }
2177 }
2178 }
2179
2180 public void sendPing() {
2181 if (!r()) {
2182 final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
2183 iq.setFrom(account.getJid());
2184 iq.addChild("ping", Namespace.PING);
2185 this.sendIqPacket(iq, null);
2186 }
2187 this.lastPingSent = SystemClock.elapsedRealtime();
2188 }
2189
2190 public void setOnMessagePacketReceivedListener(final OnMessagePacketReceived listener) {
2191 this.messageListener = listener;
2192 }
2193
2194 public void setOnUnregisteredIqPacketReceivedListener(final OnIqPacketReceived listener) {
2195 this.unregisteredIqListener = listener;
2196 }
2197
2198 public void setOnPresencePacketReceivedListener(final OnPresencePacketReceived listener) {
2199 this.presenceListener = listener;
2200 }
2201
2202 public void setOnJinglePacketReceivedListener(final OnJinglePacketReceived listener) {
2203 this.jingleListener = listener;
2204 }
2205
2206 public void setOnStatusChangedListener(final OnStatusChanged listener) {
2207 this.statusListener = listener;
2208 }
2209
2210 public void setOnBindListener(final OnBindListener listener) {
2211 this.bindListener = listener;
2212 }
2213
2214 public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) {
2215 this.acknowledgedListener = listener;
2216 }
2217
2218 public void addOnAdvancedStreamFeaturesAvailableListener(
2219 final OnAdvancedStreamFeaturesLoaded listener) {
2220 this.advancedStreamFeaturesLoadedListeners.add(listener);
2221 }
2222
2223 private void forceCloseSocket() {
2224 FileBackend.close(this.socket);
2225 FileBackend.close(this.tagReader);
2226 }
2227
2228 public void interrupt() {
2229 if (this.mThread != null) {
2230 this.mThread.interrupt();
2231 }
2232 }
2233
2234 public void disconnect(final boolean force) {
2235 interrupt();
2236 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": disconnecting force=" + force);
2237 if (force) {
2238 forceCloseSocket();
2239 } else {
2240 final TagWriter currentTagWriter = this.tagWriter;
2241 if (currentTagWriter.isActive()) {
2242 currentTagWriter.finish();
2243 final Socket currentSocket = this.socket;
2244 final CountDownLatch streamCountDownLatch = this.mStreamCountDownLatch;
2245 try {
2246 currentTagWriter.await(1, TimeUnit.SECONDS);
2247 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": closing stream");
2248 currentTagWriter.writeTag(Tag.end("stream:stream"));
2249 if (streamCountDownLatch != null) {
2250 if (streamCountDownLatch.await(1, TimeUnit.SECONDS)) {
2251 Log.d(
2252 Config.LOGTAG,
2253 account.getJid().asBareJid() + ": remote ended stream");
2254 } else {
2255 Log.d(
2256 Config.LOGTAG,
2257 account.getJid().asBareJid()
2258 + ": remote has not closed socket. force closing");
2259 }
2260 }
2261 } catch (InterruptedException e) {
2262 Log.d(
2263 Config.LOGTAG,
2264 account.getJid().asBareJid()
2265 + ": interrupted while gracefully closing stream");
2266 } catch (final IOException e) {
2267 Log.d(
2268 Config.LOGTAG,
2269 account.getJid().asBareJid()
2270 + ": io exception during disconnect ("
2271 + e.getMessage()
2272 + ")");
2273 } finally {
2274 FileBackend.close(currentSocket);
2275 }
2276 } else {
2277 forceCloseSocket();
2278 }
2279 }
2280 }
2281
2282 private void resetStreamId() {
2283 this.streamId = null;
2284 }
2285
2286 private List<Entry<Jid, ServiceDiscoveryResult>> findDiscoItemsByFeature(final String feature) {
2287 synchronized (this.disco) {
2288 final List<Entry<Jid, ServiceDiscoveryResult>> items = new ArrayList<>();
2289 for (final Entry<Jid, ServiceDiscoveryResult> cursor : this.disco.entrySet()) {
2290 if (cursor.getValue().getFeatures().contains(feature)) {
2291 items.add(cursor);
2292 }
2293 }
2294 return items;
2295 }
2296 }
2297
2298 public Jid findDiscoItemByFeature(final String feature) {
2299 final List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(feature);
2300 if (items.size() >= 1) {
2301 return items.get(0).getKey();
2302 }
2303 return null;
2304 }
2305
2306 public boolean r() {
2307 if (getFeatures().sm()) {
2308 this.tagWriter.writeStanzaAsync(new RequestPacket());
2309 return true;
2310 } else {
2311 return false;
2312 }
2313 }
2314
2315 public List<String> getMucServersWithholdAccount() {
2316 final List<String> servers = getMucServers();
2317 servers.remove(account.getDomain().toEscapedString());
2318 return servers;
2319 }
2320
2321 public List<String> getMucServers() {
2322 List<String> servers = new ArrayList<>();
2323 synchronized (this.disco) {
2324 for (final Entry<Jid, ServiceDiscoveryResult> cursor : disco.entrySet()) {
2325 final ServiceDiscoveryResult value = cursor.getValue();
2326 if (value.getFeatures().contains("http://jabber.org/protocol/muc")
2327 && value.hasIdentity("conference", "text")
2328 && !value.getFeatures().contains("jabber:iq:gateway")
2329 && !value.hasIdentity("conference", "irc")) {
2330 servers.add(cursor.getKey().toString());
2331 }
2332 }
2333 }
2334 return servers;
2335 }
2336
2337 public String getMucServer() {
2338 List<String> servers = getMucServers();
2339 return servers.size() > 0 ? servers.get(0) : null;
2340 }
2341
2342 public int getTimeToNextAttempt() {
2343 final int additionalTime =
2344 account.getLastErrorStatus() == Account.State.POLICY_VIOLATION ? 3 : 0;
2345 final int interval = Math.min((int) (25 * Math.pow(1.3, (additionalTime + attempt))), 300);
2346 final int secondsSinceLast =
2347 (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
2348 return interval - secondsSinceLast;
2349 }
2350
2351 public int getAttempt() {
2352 return this.attempt;
2353 }
2354
2355 public Features getFeatures() {
2356 return this.features;
2357 }
2358
2359 public long getLastSessionEstablished() {
2360 final long diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
2361 return System.currentTimeMillis() - diff;
2362 }
2363
2364 public long getLastConnect() {
2365 return this.lastConnect;
2366 }
2367
2368 public long getLastPingSent() {
2369 return this.lastPingSent;
2370 }
2371
2372 public long getLastDiscoStarted() {
2373 return this.lastDiscoStarted;
2374 }
2375
2376 public long getLastPacketReceived() {
2377 return this.lastPacketReceived;
2378 }
2379
2380 public void sendActive() {
2381 this.sendPacket(new ActivePacket());
2382 }
2383
2384 public void sendInactive() {
2385 this.sendPacket(new InactivePacket());
2386 }
2387
2388 public void resetAttemptCount(boolean resetConnectTime) {
2389 this.attempt = 0;
2390 if (resetConnectTime) {
2391 this.lastConnect = 0;
2392 }
2393 }
2394
2395 public void setInteractive(boolean interactive) {
2396 this.mInteractive = interactive;
2397 }
2398
2399 public Identity getServerIdentity() {
2400 synchronized (this.disco) {
2401 ServiceDiscoveryResult result = disco.get(account.getJid().getDomain());
2402 if (result == null) {
2403 return Identity.UNKNOWN;
2404 }
2405 for (final ServiceDiscoveryResult.Identity id : result.getIdentities()) {
2406 if (id.getType().equals("im")
2407 && id.getCategory().equals("server")
2408 && id.getName() != null) {
2409 switch (id.getName()) {
2410 case "Prosody":
2411 return Identity.PROSODY;
2412 case "ejabberd":
2413 return Identity.EJABBERD;
2414 case "Slack-XMPP":
2415 return Identity.SLACK;
2416 }
2417 }
2418 }
2419 }
2420 return Identity.UNKNOWN;
2421 }
2422
2423 private IqGenerator getIqGenerator() {
2424 return mXmppConnectionService.getIqGenerator();
2425 }
2426
2427 public enum Identity {
2428 FACEBOOK,
2429 SLACK,
2430 EJABBERD,
2431 PROSODY,
2432 NIMBUZZ,
2433 UNKNOWN
2434 }
2435
2436 private class MyKeyManager implements X509KeyManager {
2437 @Override
2438 public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
2439 return account.getPrivateKeyAlias();
2440 }
2441
2442 @Override
2443 public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
2444 return null;
2445 }
2446
2447 @Override
2448 public X509Certificate[] getCertificateChain(String alias) {
2449 Log.d(Config.LOGTAG, "getting certificate chain");
2450 try {
2451 return KeyChain.getCertificateChain(mXmppConnectionService, alias);
2452 } catch (final Exception e) {
2453 Log.d(Config.LOGTAG, "could not get certificate chain", e);
2454 return new X509Certificate[0];
2455 }
2456 }
2457
2458 @Override
2459 public String[] getClientAliases(String s, Principal[] principals) {
2460 final String alias = account.getPrivateKeyAlias();
2461 return alias != null ? new String[] {alias} : new String[0];
2462 }
2463
2464 @Override
2465 public String[] getServerAliases(String s, Principal[] principals) {
2466 return new String[0];
2467 }
2468
2469 @Override
2470 public PrivateKey getPrivateKey(String alias) {
2471 try {
2472 return KeyChain.getPrivateKey(mXmppConnectionService, alias);
2473 } catch (Exception e) {
2474 return null;
2475 }
2476 }
2477 }
2478
2479 private static class StateChangingError extends Error {
2480 private final Account.State state;
2481
2482 public StateChangingError(Account.State state) {
2483 this.state = state;
2484 }
2485 }
2486
2487 private static class StateChangingException extends IOException {
2488 private final Account.State state;
2489
2490 public StateChangingException(Account.State state) {
2491 this.state = state;
2492 }
2493 }
2494
2495 public class Features {
2496 XmppConnection connection;
2497 private boolean carbonsEnabled = false;
2498 private boolean encryptionEnabled = false;
2499 private boolean blockListRequested = false;
2500
2501 public Features(final XmppConnection connection) {
2502 this.connection = connection;
2503 }
2504
2505 private boolean hasDiscoFeature(final Jid server, final String feature) {
2506 synchronized (XmppConnection.this.disco) {
2507 final ServiceDiscoveryResult sdr = connection.disco.get(server);
2508 return sdr != null && sdr.getFeatures().contains(feature);
2509 }
2510 }
2511
2512 public boolean carbons() {
2513 return hasDiscoFeature(account.getDomain(), Namespace.CARBONS);
2514 }
2515
2516 public boolean commands() {
2517 return hasDiscoFeature(account.getDomain(), Namespace.COMMANDS);
2518 }
2519
2520 public boolean easyOnboardingInvites() {
2521 synchronized (commands) {
2522 return commands.containsKey(Namespace.EASY_ONBOARDING_INVITE);
2523 }
2524 }
2525
2526 public boolean bookmarksConversion() {
2527 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS_CONVERSION)
2528 && pepPublishOptions();
2529 }
2530
2531 public boolean avatarConversion() {
2532 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.AVATAR_CONVERSION)
2533 && pepPublishOptions();
2534 }
2535
2536 public boolean blocking() {
2537 return hasDiscoFeature(account.getDomain(), Namespace.BLOCKING);
2538 }
2539
2540 public boolean spamReporting() {
2541 return hasDiscoFeature(account.getDomain(), "urn:xmpp:reporting:reason:spam:0");
2542 }
2543
2544 public boolean flexibleOfflineMessageRetrieval() {
2545 return hasDiscoFeature(
2546 account.getDomain(), Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL);
2547 }
2548
2549 public boolean register() {
2550 return hasDiscoFeature(account.getDomain(), Namespace.REGISTER);
2551 }
2552
2553 public boolean invite() {
2554 return connection.streamFeatures != null
2555 && connection.streamFeatures.hasChild("register", Namespace.INVITE);
2556 }
2557
2558 public boolean sm() {
2559 return streamId != null
2560 || (connection.streamFeatures != null
2561 && connection.streamFeatures.hasChild("sm"));
2562 }
2563
2564 public boolean csi() {
2565 return connection.streamFeatures != null
2566 && connection.streamFeatures.hasChild("csi", Namespace.CSI);
2567 }
2568
2569 public boolean pep() {
2570 synchronized (XmppConnection.this.disco) {
2571 ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid());
2572 return info != null && info.hasIdentity("pubsub", "pep");
2573 }
2574 }
2575
2576 public boolean pepPersistent() {
2577 synchronized (XmppConnection.this.disco) {
2578 ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid());
2579 return info != null
2580 && info.getFeatures()
2581 .contains("http://jabber.org/protocol/pubsub#persistent-items");
2582 }
2583 }
2584
2585 public boolean pepPublishOptions() {
2586 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUBSUB_PUBLISH_OPTIONS);
2587 }
2588
2589 public boolean pepOmemoWhitelisted() {
2590 return hasDiscoFeature(
2591 account.getJid().asBareJid(), AxolotlService.PEP_OMEMO_WHITELISTED);
2592 }
2593
2594 public boolean mam() {
2595 return MessageArchiveService.Version.has(getAccountFeatures());
2596 }
2597
2598 public List<String> getAccountFeatures() {
2599 ServiceDiscoveryResult result = connection.disco.get(account.getJid().asBareJid());
2600 return result == null ? Collections.emptyList() : result.getFeatures();
2601 }
2602
2603 public boolean push() {
2604 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUSH)
2605 || hasDiscoFeature(account.getDomain(), Namespace.PUSH);
2606 }
2607
2608 public boolean rosterVersioning() {
2609 return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver");
2610 }
2611
2612 public void setBlockListRequested(boolean value) {
2613 this.blockListRequested = value;
2614 }
2615
2616 public boolean httpUpload(long filesize) {
2617 if (Config.DISABLE_HTTP_UPLOAD) {
2618 return false;
2619 } else {
2620 for (String namespace :
2621 new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) {
2622 List<Entry<Jid, ServiceDiscoveryResult>> items =
2623 findDiscoItemsByFeature(namespace);
2624 if (items.size() > 0) {
2625 try {
2626 long maxsize =
2627 Long.parseLong(
2628 items.get(0)
2629 .getValue()
2630 .getExtendedDiscoInformation(
2631 namespace, "max-file-size"));
2632 if (filesize <= maxsize) {
2633 return true;
2634 } else {
2635 Log.d(
2636 Config.LOGTAG,
2637 account.getJid().asBareJid()
2638 + ": http upload is not available for files with size "
2639 + filesize
2640 + " (max is "
2641 + maxsize
2642 + ")");
2643 return false;
2644 }
2645 } catch (Exception e) {
2646 return true;
2647 }
2648 }
2649 }
2650 return false;
2651 }
2652 }
2653
2654 public boolean useLegacyHttpUpload() {
2655 return findDiscoItemByFeature(Namespace.HTTP_UPLOAD) == null
2656 && findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY) != null;
2657 }
2658
2659 public long getMaxHttpUploadSize() {
2660 for (String namespace :
2661 new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) {
2662 List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(namespace);
2663 if (items.size() > 0) {
2664 try {
2665 return Long.parseLong(
2666 items.get(0)
2667 .getValue()
2668 .getExtendedDiscoInformation(namespace, "max-file-size"));
2669 } catch (Exception e) {
2670 // ignored
2671 }
2672 }
2673 }
2674 return -1;
2675 }
2676
2677 public boolean stanzaIds() {
2678 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.STANZA_IDS);
2679 }
2680
2681 public boolean bookmarks2() {
2682 return Config
2683 .USE_BOOKMARKS2 /* || hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT)*/;
2684 }
2685
2686 public boolean externalServiceDiscovery() {
2687 return hasDiscoFeature(account.getDomain(), Namespace.EXTERNAL_SERVICE_DISCOVERY);
2688 }
2689 }
2690}