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.Strings;
19
20import org.xmlpull.v1.XmlPullParserException;
21
22import java.io.ByteArrayInputStream;
23import java.io.IOException;
24import java.io.InputStream;
25import java.net.ConnectException;
26import java.net.IDN;
27import java.net.InetAddress;
28import java.net.InetSocketAddress;
29import java.net.Socket;
30import java.net.UnknownHostException;
31import java.security.KeyManagementException;
32import java.security.NoSuchAlgorithmException;
33import java.security.Principal;
34import java.security.PrivateKey;
35import java.security.cert.X509Certificate;
36import java.util.ArrayList;
37import java.util.Arrays;
38import java.util.Collection;
39import java.util.Collections;
40import java.util.HashMap;
41import java.util.HashSet;
42import java.util.Hashtable;
43import java.util.Iterator;
44import java.util.List;
45import java.util.Map.Entry;
46import java.util.Set;
47import java.util.concurrent.CountDownLatch;
48import java.util.concurrent.TimeUnit;
49import java.util.concurrent.atomic.AtomicBoolean;
50import java.util.concurrent.atomic.AtomicInteger;
51import java.util.regex.Matcher;
52
53import javax.net.ssl.KeyManager;
54import javax.net.ssl.SSLContext;
55import javax.net.ssl.SSLPeerUnverifiedException;
56import javax.net.ssl.SSLSocket;
57import javax.net.ssl.SSLSocketFactory;
58import javax.net.ssl.X509KeyManager;
59import javax.net.ssl.X509TrustManager;
60
61import eu.siacs.conversations.Config;
62import eu.siacs.conversations.R;
63import eu.siacs.conversations.crypto.XmppDomainVerifier;
64import eu.siacs.conversations.crypto.axolotl.AxolotlService;
65import eu.siacs.conversations.crypto.sasl.ChannelBinding;
66import eu.siacs.conversations.crypto.sasl.HashedToken;
67import eu.siacs.conversations.crypto.sasl.SaslMechanism;
68import eu.siacs.conversations.entities.Account;
69import eu.siacs.conversations.entities.Message;
70import eu.siacs.conversations.entities.ServiceDiscoveryResult;
71import eu.siacs.conversations.generator.IqGenerator;
72import eu.siacs.conversations.http.HttpConnectionManager;
73import eu.siacs.conversations.persistance.FileBackend;
74import eu.siacs.conversations.services.MemorizingTrustManager;
75import eu.siacs.conversations.services.MessageArchiveService;
76import eu.siacs.conversations.services.NotificationService;
77import eu.siacs.conversations.services.XmppConnectionService;
78import eu.siacs.conversations.utils.CryptoHelper;
79import eu.siacs.conversations.utils.Patterns;
80import eu.siacs.conversations.utils.PhoneHelper;
81import eu.siacs.conversations.utils.Resolver;
82import eu.siacs.conversations.utils.SSLSockets;
83import eu.siacs.conversations.utils.SocksSocketFactory;
84import eu.siacs.conversations.utils.XmlHelper;
85import eu.siacs.conversations.xml.Element;
86import eu.siacs.conversations.xml.LocalizedContent;
87import eu.siacs.conversations.xml.Namespace;
88import eu.siacs.conversations.xml.Tag;
89import eu.siacs.conversations.xml.TagWriter;
90import eu.siacs.conversations.xml.XmlReader;
91import eu.siacs.conversations.xmpp.bind.Bind2;
92import eu.siacs.conversations.xmpp.forms.Data;
93import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
94import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
95import eu.siacs.conversations.xmpp.stanzas.AbstractAcknowledgeableStanza;
96import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
97import eu.siacs.conversations.xmpp.stanzas.IqPacket;
98import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
99import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
100import eu.siacs.conversations.xmpp.stanzas.csi.ActivePacket;
101import eu.siacs.conversations.xmpp.stanzas.csi.InactivePacket;
102import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
103import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
104import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
105import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
106import okhttp3.HttpUrl;
107
108public class XmppConnection implements Runnable {
109
110 private static final int PACKET_IQ = 0;
111 private static final int PACKET_MESSAGE = 1;
112 private static final int PACKET_PRESENCE = 2;
113 public final OnIqPacketReceived registrationResponseListener =
114 (account, packet) -> {
115 if (packet.getType() == IqPacket.TYPE.RESULT) {
116 account.setOption(Account.OPTION_REGISTER, false);
117 Log.d(
118 Config.LOGTAG,
119 account.getJid().asBareJid()
120 + ": successfully registered new account on server");
121 throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL);
122 } else {
123 final List<String> PASSWORD_TOO_WEAK_MSGS =
124 Arrays.asList(
125 "The password is too weak", "Please use a longer password.");
126 Element error = packet.findChild("error");
127 Account.State state = Account.State.REGISTRATION_FAILED;
128 if (error != null) {
129 if (error.hasChild("conflict")) {
130 state = Account.State.REGISTRATION_CONFLICT;
131 } else if (error.hasChild("resource-constraint")
132 && "wait".equals(error.getAttribute("type"))) {
133 state = Account.State.REGISTRATION_PLEASE_WAIT;
134 } else if (error.hasChild("not-acceptable")
135 && PASSWORD_TOO_WEAK_MSGS.contains(
136 error.findChildContent("text"))) {
137 state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK;
138 }
139 }
140 throw new StateChangingError(state);
141 }
142 };
143 protected final Account account;
144 private final Features features = new Features(this);
145 private final HashMap<Jid, ServiceDiscoveryResult> disco = new HashMap<>();
146 private final HashMap<String, Jid> commands = new HashMap<>();
147 private final SparseArray<AbstractAcknowledgeableStanza> mStanzaQueue = new SparseArray<>();
148 private final Hashtable<String, Pair<IqPacket, OnIqPacketReceived>> packetCallbacks =
149 new Hashtable<>();
150 private final Set<OnAdvancedStreamFeaturesLoaded> advancedStreamFeaturesLoadedListeners =
151 new HashSet<>();
152 private final XmppConnectionService mXmppConnectionService;
153 private Socket socket;
154 private XmlReader tagReader;
155 private TagWriter tagWriter = new TagWriter();
156 private boolean shouldAuthenticate = true;
157 private boolean inSmacksSession = false;
158 private boolean quickStartInProgress = false;
159 private boolean isBound = false;
160 private Element streamFeatures;
161 private String streamId = null;
162 private int stanzasReceived = 0;
163 private int stanzasSent = 0;
164 private long lastPacketReceived = 0;
165 private long lastPingSent = 0;
166 private long lastConnect = 0;
167 private long lastSessionStarted = 0;
168 private long lastDiscoStarted = 0;
169 private boolean isMamPreferenceAlways = false;
170 private final AtomicInteger mPendingServiceDiscoveries = new AtomicInteger(0);
171 private final AtomicBoolean mWaitForDisco = new AtomicBoolean(true);
172 private final AtomicBoolean mWaitingForSmCatchup = new AtomicBoolean(false);
173 private final AtomicInteger mSmCatchupMessageCounter = new AtomicInteger(0);
174 private boolean mInteractive = false;
175 private int attempt = 0;
176 private OnPresencePacketReceived presenceListener = null;
177 private OnJinglePacketReceived jingleListener = null;
178 private OnIqPacketReceived unregisteredIqListener = null;
179 private OnMessagePacketReceived messageListener = null;
180 private OnStatusChanged statusListener = null;
181 private OnBindListener bindListener = null;
182 private OnMessageAcknowledged acknowledgedListener = null;
183 private SaslMechanism saslMechanism;
184 private HashedToken.Mechanism hashTokenRequest;
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 final SSLSocket sslSocket = (SSLSocket) socket;
498 SSLSockets.log(account, sslSocket);
499 quickStart = establishStream(SSLSockets.version(sslSocket));
500 } else {
501 quickStart = establishStream(SSLSockets.Version.NONE);
502 }
503 final Tag tag = tagReader.readTag();
504 if (Thread.currentThread().isInterrupted()) {
505 throw new InterruptedException();
506 }
507 final boolean success = tag != null && tag.isStart("stream", Namespace.STREAMS);
508 if (success && quickStart) {
509 this.quickStartInProgress = true;
510 }
511 return success;
512 }
513
514 private SSLSocketFactory getSSLSocketFactory()
515 throws NoSuchAlgorithmException, KeyManagementException {
516 final SSLContext sc = SSLSockets.getSSLContext();
517 final MemorizingTrustManager trustManager =
518 this.mXmppConnectionService.getMemorizingTrustManager();
519 final KeyManager[] keyManager;
520 if (account.getPrivateKeyAlias() != null) {
521 keyManager = new KeyManager[] {new MyKeyManager()};
522 } else {
523 keyManager = null;
524 }
525 final String domain = account.getServer();
526 sc.init(
527 keyManager,
528 new X509TrustManager[] {
529 mInteractive
530 ? trustManager.getInteractive(domain)
531 : trustManager.getNonInteractive(domain)
532 },
533 SECURE_RANDOM);
534 return sc.getSocketFactory();
535 }
536
537 @Override
538 public void run() {
539 synchronized (this) {
540 this.mThread = Thread.currentThread();
541 if (this.mThread.isInterrupted()) {
542 Log.d(
543 Config.LOGTAG,
544 account.getJid().asBareJid()
545 + ": aborting connect because thread was interrupted");
546 return;
547 }
548 forceCloseSocket();
549 }
550 connect();
551 }
552
553 private void processStream() throws XmlPullParserException, IOException {
554 final CountDownLatch streamCountDownLatch = new CountDownLatch(1);
555 this.mStreamCountDownLatch = streamCountDownLatch;
556 Tag nextTag = tagReader.readTag();
557 while (nextTag != null && !nextTag.isEnd("stream")) {
558 if (nextTag.isStart("error")) {
559 processStreamError(nextTag);
560 } else if (nextTag.isStart("features", Namespace.STREAMS)) {
561 processStreamFeatures(nextTag);
562 } else if (nextTag.isStart("proceed", Namespace.TLS)) {
563 switchOverToTls();
564 } else if (nextTag.isStart("success")) {
565 final Element success = tagReader.readElement(nextTag);
566 if (processSuccess(success)) {
567 break;
568 }
569
570 } else if (nextTag.isStart("failure", Namespace.TLS)) {
571 throw new StateChangingException(Account.State.TLS_ERROR);
572 } else if (nextTag.isStart("failure")) {
573 final Element failure = tagReader.readElement(nextTag);
574 processFailure(failure);
575 } else if (nextTag.isStart("continue", Namespace.SASL_2)) {
576 // two step sasl2 - we don’t support this yet
577 throw new StateChangingException(Account.State.INCOMPATIBLE_CLIENT);
578 } else if (nextTag.isStart("challenge")) {
579 final Element challenge = tagReader.readElement(nextTag);
580 processChallenge(challenge);
581 } else if (nextTag.isStart("enabled", Namespace.STREAM_MANAGEMENT)) {
582 final Element enabled = tagReader.readElement(nextTag);
583 processEnabled(enabled);
584 } else if (nextTag.isStart("resumed")) {
585 final Element resumed = tagReader.readElement(nextTag);
586 processResumed(resumed);
587 } else if (nextTag.isStart("r")) {
588 tagReader.readElement(nextTag);
589 if (Config.EXTENDED_SM_LOGGING) {
590 Log.d(
591 Config.LOGTAG,
592 account.getJid().asBareJid()
593 + ": acknowledging stanza #"
594 + this.stanzasReceived);
595 }
596 final AckPacket ack = new AckPacket(this.stanzasReceived);
597 tagWriter.writeStanzaAsync(ack);
598 } else if (nextTag.isStart("a")) {
599 boolean accountUiNeedsRefresh = false;
600 synchronized (NotificationService.CATCHUP_LOCK) {
601 if (mWaitingForSmCatchup.compareAndSet(true, false)) {
602 final int messageCount = mSmCatchupMessageCounter.get();
603 final int pendingIQs = packetCallbacks.size();
604 Log.d(
605 Config.LOGTAG,
606 account.getJid().asBareJid()
607 + ": SM catchup complete (messages="
608 + messageCount
609 + ", pending IQs="
610 + pendingIQs
611 + ")");
612 accountUiNeedsRefresh = true;
613 if (messageCount > 0) {
614 mXmppConnectionService
615 .getNotificationService()
616 .finishBacklog(true, account);
617 }
618 }
619 }
620 if (accountUiNeedsRefresh) {
621 mXmppConnectionService.updateAccountUi();
622 }
623 final Element ack = tagReader.readElement(nextTag);
624 lastPacketReceived = SystemClock.elapsedRealtime();
625 try {
626 final boolean acknowledgedMessages;
627 synchronized (this.mStanzaQueue) {
628 final int serverSequence = Integer.parseInt(ack.getAttribute("h"));
629 acknowledgedMessages = acknowledgeStanzaUpTo(serverSequence);
630 }
631 if (acknowledgedMessages) {
632 mXmppConnectionService.updateConversationUi();
633 }
634 } catch (NumberFormatException | NullPointerException e) {
635 Log.d(
636 Config.LOGTAG,
637 account.getJid().asBareJid()
638 + ": server send ack without sequence number");
639 }
640 } else if (nextTag.isStart("failed")) {
641 final Element failed = tagReader.readElement(nextTag);
642 processFailed(failed, true);
643 } else if (nextTag.isStart("iq")) {
644 processIq(nextTag);
645 } else if (nextTag.isStart("message")) {
646 processMessage(nextTag);
647 } else if (nextTag.isStart("presence")) {
648 processPresence(nextTag);
649 }
650 nextTag = tagReader.readTag();
651 }
652 if (nextTag != null && nextTag.isEnd("stream")) {
653 streamCountDownLatch.countDown();
654 }
655 }
656
657 private void processChallenge(Element challenge) throws IOException {
658 final SaslMechanism.Version version;
659 try {
660 version = SaslMechanism.Version.of(challenge);
661 } catch (final IllegalArgumentException e) {
662 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
663 }
664 final Element response;
665 if (version == SaslMechanism.Version.SASL) {
666 response = new Element("response", Namespace.SASL);
667 } else if (version == SaslMechanism.Version.SASL_2) {
668 response = new Element("response", Namespace.SASL_2);
669 } else {
670 throw new AssertionError("Missing implementation for " + version);
671 }
672 try {
673 response.setContent(saslMechanism.getResponse(challenge.getContent(), sslSocketOrNull(socket)));
674 } catch (final SaslMechanism.AuthenticationException e) {
675 // TODO: Send auth abort tag.
676 Log.e(Config.LOGTAG, e.toString());
677 throw new StateChangingException(Account.State.UNAUTHORIZED);
678 }
679 tagWriter.writeElement(response);
680 }
681
682 private boolean processSuccess(final Element success)
683 throws IOException, XmlPullParserException {
684 final SaslMechanism.Version version;
685 try {
686 version = SaslMechanism.Version.of(success);
687 } catch (final IllegalArgumentException e) {
688 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
689 }
690 final String challenge;
691 if (version == SaslMechanism.Version.SASL) {
692 challenge = success.getContent();
693 } else if (version == SaslMechanism.Version.SASL_2) {
694 challenge = success.findChildContent("additional-data");
695 } else {
696 throw new AssertionError("Missing implementation for " + version);
697 }
698 try {
699 saslMechanism.getResponse(challenge, sslSocketOrNull(socket));
700 } catch (final SaslMechanism.AuthenticationException e) {
701 Log.e(Config.LOGTAG, String.valueOf(e));
702 throw new StateChangingException(Account.State.UNAUTHORIZED);
703 }
704 Log.d(
705 Config.LOGTAG,
706 account.getJid().asBareJid().toString() + ": logged in (using " + version + ")");
707 account.setPinnedMechanism(saslMechanism);
708 if (version == SaslMechanism.Version.SASL_2) {
709 final Tag tag = tagReader.readTag();
710 if (tag != null && tag.isStart("features", Namespace.STREAMS)) {
711 this.streamFeatures = tagReader.readElement(tag);
712 Log.d(
713 Config.LOGTAG,
714 account.getJid().asBareJid()
715 + ": processed NOP stream features after success "
716 + XmlHelper.printElementNames(this.streamFeatures));
717 } else {
718 Log.d(
719 Config.LOGTAG,
720 account.getJid().asBareJid()
721 + ": server did not send stream features after SASL2 success");
722 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
723 }
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 final Element tokenWrapper = success.findChild("token", Namespace.FAST);
766 final String token = tokenWrapper == null ? null : tokenWrapper.getAttribute("token");
767 if (bound != null && resumed != null) {
768 Log.d(
769 Config.LOGTAG,
770 account.getJid().asBareJid()
771 + ": server sent bound and resumed in SASL2 success");
772 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
773 }
774 if (resumed != null && streamId != null) {
775 processResumed(resumed);
776 } else if (failed != null) {
777 processFailed(failed, false); // wait for new stream features
778 }
779 if (bound != null) {
780 clearIqCallbacks();
781 this.isBound = true;
782 final Element streamManagementEnabled =
783 bound.findChild("enabled", Namespace.STREAM_MANAGEMENT);
784 final Element carbonsEnabled = bound.findChild("enabled", Namespace.CARBONS);
785 final boolean waitForDisco;
786 if (streamManagementEnabled != null) {
787 processEnabled(streamManagementEnabled);
788 waitForDisco = true;
789 } else {
790 //if we did not enable stream management in bind do it now
791 waitForDisco = enableStreamManagement();
792 }
793 if (carbonsEnabled != null) {
794 Log.d(
795 Config.LOGTAG,
796 account.getJid().asBareJid() + ": successfully enabled carbons");
797 features.carbonsEnabled = true;
798 }
799 sendPostBindInitialization(waitForDisco, carbonsEnabled != null);
800 }
801 //TODO figure out name either by the existence of hashTokenRequest or if scramMechanism is of instance HashedToken
802 if (this.hashTokenRequest != null && !Strings.isNullOrEmpty(token)) {
803 Log.d(Config.LOGTAG,account.getJid().asBareJid()+": storing hashed token "+this.hashTokenRequest.name()+ " "+token);
804 }
805 }
806 this.quickStartInProgress = false;
807 if (version == SaslMechanism.Version.SASL) {
808 tagReader.reset();
809 sendStartStream(false, true);
810 final Tag tag = tagReader.readTag();
811 if (tag != null && tag.isStart("stream", Namespace.STREAMS)) {
812 processStream();
813 return true;
814 } else {
815 throw new StateChangingException(Account.State.STREAM_OPENING_ERROR);
816 }
817 } else {
818 return false;
819 }
820 }
821
822 private void processFailure(final Element failure) throws StateChangingException {
823 final SaslMechanism.Version version;
824 try {
825 version = SaslMechanism.Version.of(failure);
826 } catch (final IllegalArgumentException e) {
827 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
828 }
829 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version);
830 if (failure.hasChild("temporary-auth-failure")) {
831 throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE);
832 } else if (failure.hasChild("account-disabled")) {
833 final String text = failure.findChildContent("text");
834 if (Strings.isNullOrEmpty(text)) {
835 throw new StateChangingException(Account.State.UNAUTHORIZED);
836 }
837 final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(text);
838 if (matcher.find()) {
839 final HttpUrl url;
840 try {
841 url = HttpUrl.get(text.substring(matcher.start(), matcher.end()));
842 } catch (final IllegalArgumentException e) {
843 throw new StateChangingException(Account.State.UNAUTHORIZED);
844 }
845 if (url.isHttps()) {
846 this.redirectionUrl = url;
847 throw new StateChangingException(Account.State.PAYMENT_REQUIRED);
848 }
849 }
850 }
851 throw new StateChangingException(Account.State.UNAUTHORIZED);
852 }
853
854 private static SSLSocket sslSocketOrNull(final Socket socket) {
855 if (socket instanceof SSLSocket) {
856 return (SSLSocket) socket;
857 } else {
858 return null;
859 }
860 }
861
862 private void processEnabled(final Element enabled) {
863 final String streamId;
864 if (enabled.getAttributeAsBoolean("resume")) {
865 streamId = enabled.getAttribute("id");
866 Log.d(
867 Config.LOGTAG,
868 account.getJid().asBareJid().toString()
869 + ": stream management enabled (resumable)");
870 } else {
871 Log.d(
872 Config.LOGTAG,
873 account.getJid().asBareJid().toString() + ": stream management enabled");
874 streamId = null;
875 }
876 this.streamId = streamId;
877 this.stanzasReceived = 0;
878 this.inSmacksSession = true;
879 final RequestPacket r = new RequestPacket();
880 tagWriter.writeStanzaAsync(r);
881 }
882
883 private void processResumed(final Element resumed) throws StateChangingException {
884 this.inSmacksSession = true;
885 this.isBound = true;
886 this.tagWriter.writeStanzaAsync(new RequestPacket());
887 lastPacketReceived = SystemClock.elapsedRealtime();
888 final String h = resumed.getAttribute("h");
889 if (h == null) {
890 resetStreamId();
891 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
892 }
893 final int serverCount;
894 try {
895 serverCount = Integer.parseInt(h);
896 } catch (final NumberFormatException e) {
897 resetStreamId();
898 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
899 }
900 final ArrayList<AbstractAcknowledgeableStanza> failedStanzas = new ArrayList<>();
901 final boolean acknowledgedMessages;
902 synchronized (this.mStanzaQueue) {
903 if (serverCount < stanzasSent) {
904 Log.d(
905 Config.LOGTAG,
906 account.getJid().asBareJid() + ": session resumed with lost packages");
907 stanzasSent = serverCount;
908 } else {
909 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": session resumed");
910 }
911 acknowledgedMessages = acknowledgeStanzaUpTo(serverCount);
912 for (int i = 0; i < this.mStanzaQueue.size(); ++i) {
913 failedStanzas.add(mStanzaQueue.valueAt(i));
914 }
915 mStanzaQueue.clear();
916 }
917 if (acknowledgedMessages) {
918 mXmppConnectionService.updateConversationUi();
919 }
920 Log.d(
921 Config.LOGTAG,
922 account.getJid().asBareJid() + ": resending " + failedStanzas.size() + " stanzas");
923 for (final AbstractAcknowledgeableStanza packet : failedStanzas) {
924 if (packet instanceof MessagePacket) {
925 MessagePacket message = (MessagePacket) packet;
926 mXmppConnectionService.markMessage(
927 account,
928 message.getTo().asBareJid(),
929 message.getId(),
930 Message.STATUS_UNSEND);
931 }
932 sendPacket(packet);
933 }
934 changeStatusToOnline();
935 }
936
937 private void changeStatusToOnline() {
938 Log.d(
939 Config.LOGTAG,
940 account.getJid().asBareJid() + ": online with resource " + account.getResource());
941 changeStatus(Account.State.ONLINE);
942 }
943
944 private void processFailed(final Element failed, final boolean sendBindRequest) {
945 final int serverCount;
946 try {
947 serverCount = Integer.parseInt(failed.getAttribute("h"));
948 } catch (final NumberFormatException | NullPointerException e) {
949 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resumption failed");
950 resetStreamId();
951 if (sendBindRequest) {
952 sendBindRequest();
953 }
954 return;
955 }
956 Log.d(
957 Config.LOGTAG,
958 account.getJid().asBareJid()
959 + ": resumption failed but server acknowledged stanza #"
960 + serverCount);
961 final boolean acknowledgedMessages;
962 synchronized (this.mStanzaQueue) {
963 acknowledgedMessages = acknowledgeStanzaUpTo(serverCount);
964 }
965 if (acknowledgedMessages) {
966 mXmppConnectionService.updateConversationUi();
967 }
968 resetStreamId();
969 if (sendBindRequest) {
970 sendBindRequest();
971 }
972 }
973
974 private boolean acknowledgeStanzaUpTo(int serverCount) {
975 if (serverCount > stanzasSent) {
976 Log.e(
977 Config.LOGTAG,
978 "server acknowledged more stanzas than we sent. serverCount="
979 + serverCount
980 + ", ourCount="
981 + stanzasSent);
982 }
983 boolean acknowledgedMessages = false;
984 for (int i = 0; i < mStanzaQueue.size(); ++i) {
985 if (serverCount >= mStanzaQueue.keyAt(i)) {
986 if (Config.EXTENDED_SM_LOGGING) {
987 Log.d(
988 Config.LOGTAG,
989 account.getJid().asBareJid()
990 + ": server acknowledged stanza #"
991 + mStanzaQueue.keyAt(i));
992 }
993 final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i);
994 if (stanza instanceof MessagePacket && acknowledgedListener != null) {
995 final MessagePacket packet = (MessagePacket) stanza;
996 final String id = packet.getId();
997 final Jid to = packet.getTo();
998 if (id != null && to != null) {
999 acknowledgedMessages |=
1000 acknowledgedListener.onMessageAcknowledged(account, to, id);
1001 }
1002 }
1003 mStanzaQueue.removeAt(i);
1004 i--;
1005 }
1006 }
1007 return acknowledgedMessages;
1008 }
1009
1010 private @NonNull Element processPacket(final Tag currentTag, final int packetType)
1011 throws IOException {
1012 final Element element;
1013 switch (packetType) {
1014 case PACKET_IQ:
1015 element = new IqPacket();
1016 break;
1017 case PACKET_MESSAGE:
1018 element = new MessagePacket();
1019 break;
1020 case PACKET_PRESENCE:
1021 element = new PresencePacket();
1022 break;
1023 default:
1024 throw new AssertionError("Should never encounter invalid type");
1025 }
1026 element.setAttributes(currentTag.getAttributes());
1027 Tag nextTag = tagReader.readTag();
1028 if (nextTag == null) {
1029 throw new IOException("interrupted mid tag");
1030 }
1031 while (!nextTag.isEnd(element.getName())) {
1032 if (!nextTag.isNo()) {
1033 element.addChild(tagReader.readElement(nextTag));
1034 }
1035 nextTag = tagReader.readTag();
1036 if (nextTag == null) {
1037 throw new IOException("interrupted mid tag");
1038 }
1039 }
1040 if (stanzasReceived == Integer.MAX_VALUE) {
1041 resetStreamId();
1042 throw new IOException("time to restart the session. cant handle >2 billion pcks");
1043 }
1044 if (inSmacksSession) {
1045 ++stanzasReceived;
1046 } else if (features.sm()) {
1047 Log.d(
1048 Config.LOGTAG,
1049 account.getJid().asBareJid()
1050 + ": not counting stanza("
1051 + element.getClass().getSimpleName()
1052 + "). Not in smacks session.");
1053 }
1054 lastPacketReceived = SystemClock.elapsedRealtime();
1055 if (Config.BACKGROUND_STANZA_LOGGING && mXmppConnectionService.checkListeners()) {
1056 Log.d(Config.LOGTAG, "[background stanza] " + element);
1057 }
1058 if (element instanceof IqPacket
1059 && (((IqPacket) element).getType() == IqPacket.TYPE.SET)
1060 && element.hasChild("jingle", Namespace.JINGLE)) {
1061 return JinglePacket.upgrade((IqPacket) element);
1062 } else {
1063 return element;
1064 }
1065 }
1066
1067 private void processIq(final Tag currentTag) throws IOException {
1068 final IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
1069 if (!packet.valid()) {
1070 Log.e(
1071 Config.LOGTAG,
1072 "encountered invalid iq from='"
1073 + packet.getFrom()
1074 + "' to='"
1075 + packet.getTo()
1076 + "'");
1077 return;
1078 }
1079 if (packet instanceof JinglePacket) {
1080 if (this.jingleListener != null) {
1081 this.jingleListener.onJinglePacketReceived(account, (JinglePacket) packet);
1082 }
1083 } else {
1084 OnIqPacketReceived callback = null;
1085 synchronized (this.packetCallbacks) {
1086 final Pair<IqPacket, OnIqPacketReceived> packetCallbackDuple =
1087 packetCallbacks.get(packet.getId());
1088 if (packetCallbackDuple != null) {
1089 // Packets to the server should have responses from the server
1090 if (packetCallbackDuple.first.toServer(account)) {
1091 if (packet.fromServer(account)) {
1092 callback = packetCallbackDuple.second;
1093 packetCallbacks.remove(packet.getId());
1094 } else {
1095 Log.e(
1096 Config.LOGTAG,
1097 account.getJid().asBareJid().toString()
1098 + ": ignoring spoofed iq packet");
1099 }
1100 } else {
1101 if (packet.getFrom() != null
1102 && packet.getFrom().equals(packetCallbackDuple.first.getTo())) {
1103 callback = packetCallbackDuple.second;
1104 packetCallbacks.remove(packet.getId());
1105 } else {
1106 Log.e(
1107 Config.LOGTAG,
1108 account.getJid().asBareJid().toString()
1109 + ": ignoring spoofed iq packet");
1110 }
1111 }
1112 } else if (packet.getType() == IqPacket.TYPE.GET
1113 || packet.getType() == IqPacket.TYPE.SET) {
1114 callback = this.unregisteredIqListener;
1115 }
1116 }
1117 if (callback != null) {
1118 try {
1119 callback.onIqPacketReceived(account, packet);
1120 } catch (StateChangingError error) {
1121 throw new StateChangingException(error.state);
1122 }
1123 }
1124 }
1125 }
1126
1127 private void processMessage(final Tag currentTag) throws IOException {
1128 final MessagePacket packet = (MessagePacket) processPacket(currentTag, PACKET_MESSAGE);
1129 if (!packet.valid()) {
1130 Log.e(
1131 Config.LOGTAG,
1132 "encountered invalid message from='"
1133 + packet.getFrom()
1134 + "' to='"
1135 + packet.getTo()
1136 + "'");
1137 return;
1138 }
1139 this.messageListener.onMessagePacketReceived(account, packet);
1140 }
1141
1142 private void processPresence(final Tag currentTag) throws IOException {
1143 PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE);
1144 if (!packet.valid()) {
1145 Log.e(
1146 Config.LOGTAG,
1147 "encountered invalid presence from='"
1148 + packet.getFrom()
1149 + "' to='"
1150 + packet.getTo()
1151 + "'");
1152 return;
1153 }
1154 this.presenceListener.onPresencePacketReceived(account, packet);
1155 }
1156
1157 private void sendStartTLS() throws IOException {
1158 final Tag startTLS = Tag.empty("starttls");
1159 startTLS.setAttribute("xmlns", Namespace.TLS);
1160 tagWriter.writeTag(startTLS);
1161 }
1162
1163 private void switchOverToTls() throws XmlPullParserException, IOException {
1164 tagReader.readTag();
1165 final Socket socket = this.socket;
1166 final SSLSocket sslSocket = upgradeSocketToTls(socket);
1167 tagReader.setInputStream(sslSocket.getInputStream());
1168 tagWriter.setOutputStream(sslSocket.getOutputStream());
1169 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS connection established");
1170 final boolean quickStart;
1171 try {
1172 quickStart = establishStream(SSLSockets.version(sslSocket));
1173 } catch (final InterruptedException e) {
1174 return;
1175 }
1176 if (quickStart) {
1177 this.quickStartInProgress = true;
1178 }
1179 features.encryptionEnabled = true;
1180 final Tag tag = tagReader.readTag();
1181 if (tag != null && tag.isStart("stream", Namespace.STREAMS)) {
1182 SSLSockets.log(account, sslSocket);
1183 processStream();
1184 } else {
1185 throw new StateChangingException(Account.State.STREAM_OPENING_ERROR);
1186 }
1187 sslSocket.close();
1188 }
1189
1190 private SSLSocket upgradeSocketToTls(final Socket socket) throws IOException {
1191 final SSLSocketFactory sslSocketFactory;
1192 try {
1193 sslSocketFactory = getSSLSocketFactory();
1194 } catch (final NoSuchAlgorithmException | KeyManagementException e) {
1195 throw new StateChangingException(Account.State.TLS_ERROR);
1196 }
1197 final InetAddress address = socket.getInetAddress();
1198 final SSLSocket sslSocket =
1199 (SSLSocket)
1200 sslSocketFactory.createSocket(
1201 socket, address.getHostAddress(), socket.getPort(), true);
1202 SSLSockets.setSecurity(sslSocket);
1203 SSLSockets.setHostname(sslSocket, IDN.toASCII(account.getServer()));
1204 SSLSockets.setApplicationProtocol(sslSocket, "xmpp-client");
1205 final XmppDomainVerifier xmppDomainVerifier = new XmppDomainVerifier();
1206 try {
1207 if (!xmppDomainVerifier.verify(
1208 account.getServer(), this.verifiedHostname, sslSocket.getSession())) {
1209 Log.d(
1210 Config.LOGTAG,
1211 account.getJid().asBareJid()
1212 + ": TLS certificate domain verification failed");
1213 FileBackend.close(sslSocket);
1214 throw new StateChangingException(Account.State.TLS_ERROR_DOMAIN);
1215 }
1216 } catch (final SSLPeerUnverifiedException e) {
1217 FileBackend.close(sslSocket);
1218 throw new StateChangingException(Account.State.TLS_ERROR);
1219 }
1220 return sslSocket;
1221 }
1222
1223 private void processStreamFeatures(final Tag currentTag) throws IOException {
1224 this.streamFeatures = tagReader.readElement(currentTag);
1225 final boolean isSecure =
1226 features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion();
1227 final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER);
1228 if (this.quickStartInProgress) {
1229 if (this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) {
1230 Log.d(
1231 Config.LOGTAG,
1232 account.getJid().asBareJid()
1233 + ": quick start in progress. ignoring features: "
1234 + XmlHelper.printElementNames(this.streamFeatures));
1235 return;
1236 }
1237 Log.d(Config.LOGTAG,account.getJid().asBareJid()+": server lost support for SASL 2. quick start not possible");
1238 this.account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, false);
1239 mXmppConnectionService.updateAccount(account);
1240 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1241 }
1242 if (this.streamFeatures.hasChild("starttls", Namespace.TLS)
1243 && !features.encryptionEnabled) {
1244 sendStartTLS();
1245 } else if (this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE)
1246 && account.isOptionSet(Account.OPTION_REGISTER)) {
1247 if (isSecure) {
1248 register();
1249 } else {
1250 Log.d(
1251 Config.LOGTAG,
1252 account.getJid().asBareJid()
1253 + ": unable to find STARTTLS for registration process "
1254 + XmlHelper.printElementNames(this.streamFeatures));
1255 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1256 }
1257 } else if (!this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE)
1258 && account.isOptionSet(Account.OPTION_REGISTER)) {
1259 throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED);
1260 } else if (this.streamFeatures.hasChild("authentication", Namespace.SASL_2)
1261 && shouldAuthenticate
1262 && isSecure) {
1263 authenticate(SaslMechanism.Version.SASL_2);
1264 } else if (this.streamFeatures.hasChild("mechanisms", Namespace.SASL)
1265 && shouldAuthenticate
1266 && isSecure) {
1267 authenticate(SaslMechanism.Version.SASL);
1268 } else if (this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT)
1269 && streamId != null
1270 && !inSmacksSession) {
1271 if (Config.EXTENDED_SM_LOGGING) {
1272 Log.d(
1273 Config.LOGTAG,
1274 account.getJid().asBareJid()
1275 + ": resuming after stanza #"
1276 + stanzasReceived);
1277 }
1278 final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived);
1279 this.mSmCatchupMessageCounter.set(0);
1280 this.mWaitingForSmCatchup.set(true);
1281 this.tagWriter.writeStanzaAsync(resume);
1282 } else if (needsBinding) {
1283 if (this.streamFeatures.hasChild("bind", Namespace.BIND) && isSecure) {
1284 sendBindRequest();
1285 } else {
1286 Log.d(
1287 Config.LOGTAG,
1288 account.getJid().asBareJid()
1289 + ": unable to find bind feature "
1290 + XmlHelper.printElementNames(this.streamFeatures));
1291 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1292 }
1293 } else {
1294 Log.d(
1295 Config.LOGTAG,
1296 account.getJid().asBareJid()
1297 + ": received NOP stream features "
1298 + XmlHelper.printElementNames(this.streamFeatures));
1299 }
1300 }
1301
1302 private void authenticate(final SaslMechanism.Version version) throws IOException {
1303 final Element authElement;
1304 if (version == SaslMechanism.Version.SASL) {
1305 authElement = this.streamFeatures.findChild("mechanisms", Namespace.SASL);
1306 } else {
1307 authElement = this.streamFeatures.findChild("authentication", Namespace.SASL_2);
1308 }
1309 final Collection<String> mechanisms = SaslMechanism.mechanisms(authElement);
1310 final Element cbElement =
1311 this.streamFeatures.findChild("sasl-channel-binding", Namespace.CHANNEL_BINDING);
1312 final Collection<ChannelBinding> channelBindings = ChannelBinding.of(cbElement);
1313 Log.d(Config.LOGTAG,"mechanisms: "+mechanisms);
1314 Log.d(Config.LOGTAG, "channel bindings: " + channelBindings);
1315 final SaslMechanism.Factory factory = new SaslMechanism.Factory(account);
1316 this.saslMechanism = factory.of(mechanisms, channelBindings, SSLSockets.version(this.socket));
1317
1318 //TODO externalize checks
1319
1320 if (saslMechanism == null) {
1321 Log.d(
1322 Config.LOGTAG,
1323 account.getJid().asBareJid()
1324 + ": unable to find supported SASL mechanism in "
1325 + mechanisms);
1326 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1327 }
1328 final int pinnedMechanism = account.getPinnedMechanismPriority();
1329 if (pinnedMechanism > saslMechanism.getPriority()) {
1330 Log.e(
1331 Config.LOGTAG,
1332 "Auth failed. Authentication mechanism "
1333 + saslMechanism.getMechanism()
1334 + " has lower priority ("
1335 + saslMechanism.getPriority()
1336 + ") than pinned priority ("
1337 + pinnedMechanism
1338 + "). Possible downgrade attack?");
1339 throw new StateChangingException(Account.State.DOWNGRADE_ATTACK);
1340 }
1341 final boolean quickStartAvailable;
1342 final String firstMessage = saslMechanism.getClientFirstMessage();
1343 final Element authenticate;
1344 if (version == SaslMechanism.Version.SASL) {
1345 authenticate = new Element("auth", Namespace.SASL);
1346 if (!Strings.isNullOrEmpty(firstMessage)) {
1347 authenticate.setContent(firstMessage);
1348 }
1349 quickStartAvailable = false;
1350 } else if (version == SaslMechanism.Version.SASL_2) {
1351 final Element inline = authElement.findChild("inline", Namespace.SASL_2);
1352 final boolean sm = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3");
1353 final Element fast = inline == null ? null : inline.findChild("fast", Namespace.FAST);
1354 final Collection<String> fastMechanisms = SaslMechanism.mechanisms(fast);
1355 final HashedToken.Mechanism hashTokenRequest = HashedToken.Mechanism.best(fastMechanisms, SSLSockets.version(this.socket));
1356 final Collection<String> bindFeatures = Bind2.features(inline);
1357 quickStartAvailable =
1358 sm
1359 && bindFeatures != null
1360 && bindFeatures.containsAll(Bind2.QUICKSTART_FEATURES);
1361 if (bindFeatures != null) {
1362 try {
1363 mXmppConnectionService.restoredFromDatabaseLatch.await();
1364 } catch (final InterruptedException e) {
1365 Log.d(
1366 Config.LOGTAG,
1367 account.getJid().asBareJid()
1368 + ": interrupted while waiting for DB restore during SASL2 bind");
1369 return;
1370 }
1371 }
1372 this.hashTokenRequest = hashTokenRequest;
1373 authenticate = generateAuthenticationRequest(firstMessage, hashTokenRequest, bindFeatures, sm);
1374 } else {
1375 throw new AssertionError("Missing implementation for " + version);
1376 }
1377
1378 if (account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, quickStartAvailable)) {
1379 mXmppConnectionService.updateAccount(account);
1380 }
1381
1382 Log.d(
1383 Config.LOGTAG,
1384 account.getJid().toString()
1385 + ": Authenticating with "
1386 + version
1387 + "/"
1388 + saslMechanism.getMechanism());
1389 authenticate.setAttribute("mechanism", saslMechanism.getMechanism());
1390 tagWriter.writeElement(authenticate);
1391 }
1392
1393 private Element generateAuthenticationRequest(final String firstMessage) {
1394 return generateAuthenticationRequest(firstMessage, null, Bind2.QUICKSTART_FEATURES, true);
1395 }
1396
1397 private Element generateAuthenticationRequest(
1398 final String firstMessage,
1399 final HashedToken.Mechanism hashedTokenRequest,
1400 final Collection<String> bind,
1401 final boolean inlineStreamManagement) {
1402 final Element authenticate = new Element("authenticate", Namespace.SASL_2);
1403 if (!Strings.isNullOrEmpty(firstMessage)) {
1404 authenticate.addChild("initial-response").setContent(firstMessage);
1405 }
1406 final Element userAgent = authenticate.addChild("user-agent");
1407 userAgent.setAttribute("id", account.getUuid());
1408 userAgent
1409 .addChild("software")
1410 .setContent(mXmppConnectionService.getString(R.string.app_name));
1411 if (!PhoneHelper.isEmulator()) {
1412 userAgent
1413 .addChild("device")
1414 .setContent(String.format("%s %s", Build.MANUFACTURER, Build.MODEL));
1415 }
1416 if (bind != null) {
1417 authenticate.addChild(generateBindRequest(bind));
1418 }
1419 if (inlineStreamManagement && streamId != null) {
1420 final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived);
1421 this.mSmCatchupMessageCounter.set(0);
1422 this.mWaitingForSmCatchup.set(true);
1423 authenticate.addChild(resume);
1424 }
1425 if (hashedTokenRequest != null) {
1426 authenticate.addChild("request-token", Namespace.FAST).setAttribute("mechanism", hashedTokenRequest.name());
1427 }
1428 return authenticate;
1429 }
1430
1431 private Element generateBindRequest(final Collection<String> bindFeatures) {
1432 Log.d(Config.LOGTAG, "inline bind features: " + bindFeatures);
1433 final Element bind = new Element("bind", Namespace.BIND2);
1434 bind.addChild("tag").setContent(mXmppConnectionService.getString(R.string.app_name));
1435 if (bindFeatures.contains(Namespace.CARBONS)) {
1436 bind.addChild("enable", Namespace.CARBONS);
1437 }
1438 if (bindFeatures.contains(Namespace.STREAM_MANAGEMENT)) {
1439 bind.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 SSLSockets.Version sslVersion)
2061 throws IOException, InterruptedException {
2062 final SaslMechanism pinnedMechanism =
2063 SaslMechanism.ensureAvailable(account.getPinnedMechanism(), sslVersion);
2064 final boolean secureConnection = sslVersion != SSLSockets.Version.NONE;
2065 if (secureConnection
2066 && Config.QUICKSTART_ENABLED
2067 && pinnedMechanism != null
2068 && account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) {
2069 mXmppConnectionService.restoredFromDatabaseLatch.await();
2070 this.saslMechanism = pinnedMechanism;
2071 final Element authenticate =
2072 generateAuthenticationRequest(pinnedMechanism.getClientFirstMessage());
2073 authenticate.setAttribute("mechanism", pinnedMechanism.getMechanism());
2074 sendStartStream(true, false);
2075 tagWriter.writeElement(authenticate);
2076 Log.d(
2077 Config.LOGTAG,
2078 account.getJid().toString()
2079 + ": quick start with "
2080 + pinnedMechanism.getMechanism());
2081 return true;
2082 } else {
2083 sendStartStream(secureConnection, true);
2084 return false;
2085 }
2086 }
2087
2088 private void sendStartStream(final boolean from, final boolean flush) throws IOException {
2089 final Tag stream = Tag.start("stream:stream");
2090 stream.setAttribute("to", account.getServer());
2091 if (from) {
2092 stream.setAttribute("from", account.getJid().asBareJid().toEscapedString());
2093 }
2094 stream.setAttribute("version", "1.0");
2095 stream.setAttribute("xml:lang", LocalizedContent.STREAM_LANGUAGE);
2096 stream.setAttribute("xmlns", "jabber:client");
2097 stream.setAttribute("xmlns:stream", Namespace.STREAMS);
2098 tagWriter.writeTag(stream, flush);
2099 }
2100
2101 private String createNewResource() {
2102 return mXmppConnectionService.getString(R.string.app_name) + '.' + nextRandomId(true);
2103 }
2104
2105 private String nextRandomId() {
2106 return nextRandomId(false);
2107 }
2108
2109 private String nextRandomId(final boolean s) {
2110 return CryptoHelper.random(s ? 3 : 9);
2111 }
2112
2113 public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) {
2114 packet.setFrom(account.getJid());
2115 return this.sendUnmodifiedIqPacket(packet, callback, false);
2116 }
2117
2118 public synchronized String sendUnmodifiedIqPacket(
2119 final IqPacket packet, final OnIqPacketReceived callback, boolean force) {
2120 if (packet.getId() == null) {
2121 packet.setAttribute("id", nextRandomId());
2122 }
2123 if (callback != null) {
2124 synchronized (this.packetCallbacks) {
2125 packetCallbacks.put(packet.getId(), new Pair<>(packet, callback));
2126 }
2127 }
2128 this.sendPacket(packet, force);
2129 return packet.getId();
2130 }
2131
2132 public void sendMessagePacket(final MessagePacket packet) {
2133 this.sendPacket(packet);
2134 }
2135
2136 public void sendPresencePacket(final PresencePacket packet) {
2137 this.sendPacket(packet);
2138 }
2139
2140 private synchronized void sendPacket(final AbstractStanza packet) {
2141 sendPacket(packet, false);
2142 }
2143
2144 private synchronized void sendPacket(final AbstractStanza packet, final boolean force) {
2145 if (stanzasSent == Integer.MAX_VALUE) {
2146 resetStreamId();
2147 disconnect(true);
2148 return;
2149 }
2150 synchronized (this.mStanzaQueue) {
2151 if (force || isBound) {
2152 tagWriter.writeStanzaAsync(packet);
2153 } else {
2154 Log.d(
2155 Config.LOGTAG,
2156 account.getJid().asBareJid()
2157 + " do not write stanza to unbound stream "
2158 + packet.toString());
2159 }
2160 if (packet instanceof AbstractAcknowledgeableStanza) {
2161 AbstractAcknowledgeableStanza stanza = (AbstractAcknowledgeableStanza) packet;
2162
2163 if (this.mStanzaQueue.size() != 0) {
2164 int currentHighestKey = this.mStanzaQueue.keyAt(this.mStanzaQueue.size() - 1);
2165 if (currentHighestKey != stanzasSent) {
2166 throw new AssertionError("Stanza count messed up");
2167 }
2168 }
2169
2170 ++stanzasSent;
2171 this.mStanzaQueue.append(stanzasSent, stanza);
2172 if (stanza instanceof MessagePacket && stanza.getId() != null && inSmacksSession) {
2173 if (Config.EXTENDED_SM_LOGGING) {
2174 Log.d(
2175 Config.LOGTAG,
2176 account.getJid().asBareJid()
2177 + ": requesting ack for message stanza #"
2178 + stanzasSent);
2179 }
2180 tagWriter.writeStanzaAsync(new RequestPacket());
2181 }
2182 }
2183 }
2184 }
2185
2186 public void sendPing() {
2187 if (!r()) {
2188 final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
2189 iq.setFrom(account.getJid());
2190 iq.addChild("ping", Namespace.PING);
2191 this.sendIqPacket(iq, null);
2192 }
2193 this.lastPingSent = SystemClock.elapsedRealtime();
2194 }
2195
2196 public void setOnMessagePacketReceivedListener(final OnMessagePacketReceived listener) {
2197 this.messageListener = listener;
2198 }
2199
2200 public void setOnUnregisteredIqPacketReceivedListener(final OnIqPacketReceived listener) {
2201 this.unregisteredIqListener = listener;
2202 }
2203
2204 public void setOnPresencePacketReceivedListener(final OnPresencePacketReceived listener) {
2205 this.presenceListener = listener;
2206 }
2207
2208 public void setOnJinglePacketReceivedListener(final OnJinglePacketReceived listener) {
2209 this.jingleListener = listener;
2210 }
2211
2212 public void setOnStatusChangedListener(final OnStatusChanged listener) {
2213 this.statusListener = listener;
2214 }
2215
2216 public void setOnBindListener(final OnBindListener listener) {
2217 this.bindListener = listener;
2218 }
2219
2220 public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) {
2221 this.acknowledgedListener = listener;
2222 }
2223
2224 public void addOnAdvancedStreamFeaturesAvailableListener(
2225 final OnAdvancedStreamFeaturesLoaded listener) {
2226 this.advancedStreamFeaturesLoadedListeners.add(listener);
2227 }
2228
2229 private void forceCloseSocket() {
2230 FileBackend.close(this.socket);
2231 FileBackend.close(this.tagReader);
2232 }
2233
2234 public void interrupt() {
2235 if (this.mThread != null) {
2236 this.mThread.interrupt();
2237 }
2238 }
2239
2240 public void disconnect(final boolean force) {
2241 interrupt();
2242 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": disconnecting force=" + force);
2243 if (force) {
2244 forceCloseSocket();
2245 } else {
2246 final TagWriter currentTagWriter = this.tagWriter;
2247 if (currentTagWriter.isActive()) {
2248 currentTagWriter.finish();
2249 final Socket currentSocket = this.socket;
2250 final CountDownLatch streamCountDownLatch = this.mStreamCountDownLatch;
2251 try {
2252 currentTagWriter.await(1, TimeUnit.SECONDS);
2253 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": closing stream");
2254 currentTagWriter.writeTag(Tag.end("stream:stream"));
2255 if (streamCountDownLatch != null) {
2256 if (streamCountDownLatch.await(1, TimeUnit.SECONDS)) {
2257 Log.d(
2258 Config.LOGTAG,
2259 account.getJid().asBareJid() + ": remote ended stream");
2260 } else {
2261 Log.d(
2262 Config.LOGTAG,
2263 account.getJid().asBareJid()
2264 + ": remote has not closed socket. force closing");
2265 }
2266 }
2267 } catch (InterruptedException e) {
2268 Log.d(
2269 Config.LOGTAG,
2270 account.getJid().asBareJid()
2271 + ": interrupted while gracefully closing stream");
2272 } catch (final IOException e) {
2273 Log.d(
2274 Config.LOGTAG,
2275 account.getJid().asBareJid()
2276 + ": io exception during disconnect ("
2277 + e.getMessage()
2278 + ")");
2279 } finally {
2280 FileBackend.close(currentSocket);
2281 }
2282 } else {
2283 forceCloseSocket();
2284 }
2285 }
2286 }
2287
2288 private void resetStreamId() {
2289 this.streamId = null;
2290 }
2291
2292 private List<Entry<Jid, ServiceDiscoveryResult>> findDiscoItemsByFeature(final String feature) {
2293 synchronized (this.disco) {
2294 final List<Entry<Jid, ServiceDiscoveryResult>> items = new ArrayList<>();
2295 for (final Entry<Jid, ServiceDiscoveryResult> cursor : this.disco.entrySet()) {
2296 if (cursor.getValue().getFeatures().contains(feature)) {
2297 items.add(cursor);
2298 }
2299 }
2300 return items;
2301 }
2302 }
2303
2304 public Jid findDiscoItemByFeature(final String feature) {
2305 final List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(feature);
2306 if (items.size() >= 1) {
2307 return items.get(0).getKey();
2308 }
2309 return null;
2310 }
2311
2312 public boolean r() {
2313 if (getFeatures().sm()) {
2314 this.tagWriter.writeStanzaAsync(new RequestPacket());
2315 return true;
2316 } else {
2317 return false;
2318 }
2319 }
2320
2321 public List<String> getMucServersWithholdAccount() {
2322 final List<String> servers = getMucServers();
2323 servers.remove(account.getDomain().toEscapedString());
2324 return servers;
2325 }
2326
2327 public List<String> getMucServers() {
2328 List<String> servers = new ArrayList<>();
2329 synchronized (this.disco) {
2330 for (final Entry<Jid, ServiceDiscoveryResult> cursor : disco.entrySet()) {
2331 final ServiceDiscoveryResult value = cursor.getValue();
2332 if (value.getFeatures().contains("http://jabber.org/protocol/muc")
2333 && value.hasIdentity("conference", "text")
2334 && !value.getFeatures().contains("jabber:iq:gateway")
2335 && !value.hasIdentity("conference", "irc")) {
2336 servers.add(cursor.getKey().toString());
2337 }
2338 }
2339 }
2340 return servers;
2341 }
2342
2343 public String getMucServer() {
2344 List<String> servers = getMucServers();
2345 return servers.size() > 0 ? servers.get(0) : null;
2346 }
2347
2348 public int getTimeToNextAttempt() {
2349 final int additionalTime =
2350 account.getLastErrorStatus() == Account.State.POLICY_VIOLATION ? 3 : 0;
2351 final int interval = Math.min((int) (25 * Math.pow(1.3, (additionalTime + attempt))), 300);
2352 final int secondsSinceLast =
2353 (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
2354 return interval - secondsSinceLast;
2355 }
2356
2357 public int getAttempt() {
2358 return this.attempt;
2359 }
2360
2361 public Features getFeatures() {
2362 return this.features;
2363 }
2364
2365 public long getLastSessionEstablished() {
2366 final long diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
2367 return System.currentTimeMillis() - diff;
2368 }
2369
2370 public long getLastConnect() {
2371 return this.lastConnect;
2372 }
2373
2374 public long getLastPingSent() {
2375 return this.lastPingSent;
2376 }
2377
2378 public long getLastDiscoStarted() {
2379 return this.lastDiscoStarted;
2380 }
2381
2382 public long getLastPacketReceived() {
2383 return this.lastPacketReceived;
2384 }
2385
2386 public void sendActive() {
2387 this.sendPacket(new ActivePacket());
2388 }
2389
2390 public void sendInactive() {
2391 this.sendPacket(new InactivePacket());
2392 }
2393
2394 public void resetAttemptCount(boolean resetConnectTime) {
2395 this.attempt = 0;
2396 if (resetConnectTime) {
2397 this.lastConnect = 0;
2398 }
2399 }
2400
2401 public void setInteractive(boolean interactive) {
2402 this.mInteractive = interactive;
2403 }
2404
2405 public Identity getServerIdentity() {
2406 synchronized (this.disco) {
2407 ServiceDiscoveryResult result = disco.get(account.getJid().getDomain());
2408 if (result == null) {
2409 return Identity.UNKNOWN;
2410 }
2411 for (final ServiceDiscoveryResult.Identity id : result.getIdentities()) {
2412 if (id.getType().equals("im")
2413 && id.getCategory().equals("server")
2414 && id.getName() != null) {
2415 switch (id.getName()) {
2416 case "Prosody":
2417 return Identity.PROSODY;
2418 case "ejabberd":
2419 return Identity.EJABBERD;
2420 case "Slack-XMPP":
2421 return Identity.SLACK;
2422 }
2423 }
2424 }
2425 }
2426 return Identity.UNKNOWN;
2427 }
2428
2429 private IqGenerator getIqGenerator() {
2430 return mXmppConnectionService.getIqGenerator();
2431 }
2432
2433 public enum Identity {
2434 FACEBOOK,
2435 SLACK,
2436 EJABBERD,
2437 PROSODY,
2438 NIMBUZZ,
2439 UNKNOWN
2440 }
2441
2442 private class MyKeyManager implements X509KeyManager {
2443 @Override
2444 public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
2445 return account.getPrivateKeyAlias();
2446 }
2447
2448 @Override
2449 public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
2450 return null;
2451 }
2452
2453 @Override
2454 public X509Certificate[] getCertificateChain(String alias) {
2455 Log.d(Config.LOGTAG, "getting certificate chain");
2456 try {
2457 return KeyChain.getCertificateChain(mXmppConnectionService, alias);
2458 } catch (final Exception e) {
2459 Log.d(Config.LOGTAG, "could not get certificate chain", e);
2460 return new X509Certificate[0];
2461 }
2462 }
2463
2464 @Override
2465 public String[] getClientAliases(String s, Principal[] principals) {
2466 final String alias = account.getPrivateKeyAlias();
2467 return alias != null ? new String[] {alias} : new String[0];
2468 }
2469
2470 @Override
2471 public String[] getServerAliases(String s, Principal[] principals) {
2472 return new String[0];
2473 }
2474
2475 @Override
2476 public PrivateKey getPrivateKey(String alias) {
2477 try {
2478 return KeyChain.getPrivateKey(mXmppConnectionService, alias);
2479 } catch (Exception e) {
2480 return null;
2481 }
2482 }
2483 }
2484
2485 private static class StateChangingError extends Error {
2486 private final Account.State state;
2487
2488 public StateChangingError(Account.State state) {
2489 this.state = state;
2490 }
2491 }
2492
2493 private static class StateChangingException extends IOException {
2494 private final Account.State state;
2495
2496 public StateChangingException(Account.State state) {
2497 this.state = state;
2498 }
2499 }
2500
2501 public class Features {
2502 XmppConnection connection;
2503 private boolean carbonsEnabled = false;
2504 private boolean encryptionEnabled = false;
2505 private boolean blockListRequested = false;
2506
2507 public Features(final XmppConnection connection) {
2508 this.connection = connection;
2509 }
2510
2511 private boolean hasDiscoFeature(final Jid server, final String feature) {
2512 synchronized (XmppConnection.this.disco) {
2513 final ServiceDiscoveryResult sdr = connection.disco.get(server);
2514 return sdr != null && sdr.getFeatures().contains(feature);
2515 }
2516 }
2517
2518 public boolean carbons() {
2519 return hasDiscoFeature(account.getDomain(), Namespace.CARBONS);
2520 }
2521
2522 public boolean commands() {
2523 return hasDiscoFeature(account.getDomain(), Namespace.COMMANDS);
2524 }
2525
2526 public boolean easyOnboardingInvites() {
2527 synchronized (commands) {
2528 return commands.containsKey(Namespace.EASY_ONBOARDING_INVITE);
2529 }
2530 }
2531
2532 public boolean bookmarksConversion() {
2533 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS_CONVERSION)
2534 && pepPublishOptions();
2535 }
2536
2537 public boolean avatarConversion() {
2538 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.AVATAR_CONVERSION)
2539 && pepPublishOptions();
2540 }
2541
2542 public boolean blocking() {
2543 return hasDiscoFeature(account.getDomain(), Namespace.BLOCKING);
2544 }
2545
2546 public boolean spamReporting() {
2547 return hasDiscoFeature(account.getDomain(), "urn:xmpp:reporting:reason:spam:0");
2548 }
2549
2550 public boolean flexibleOfflineMessageRetrieval() {
2551 return hasDiscoFeature(
2552 account.getDomain(), Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL);
2553 }
2554
2555 public boolean register() {
2556 return hasDiscoFeature(account.getDomain(), Namespace.REGISTER);
2557 }
2558
2559 public boolean invite() {
2560 return connection.streamFeatures != null
2561 && connection.streamFeatures.hasChild("register", Namespace.INVITE);
2562 }
2563
2564 public boolean sm() {
2565 return streamId != null
2566 || (connection.streamFeatures != null
2567 && connection.streamFeatures.hasChild("sm"));
2568 }
2569
2570 public boolean csi() {
2571 return connection.streamFeatures != null
2572 && connection.streamFeatures.hasChild("csi", Namespace.CSI);
2573 }
2574
2575 public boolean pep() {
2576 synchronized (XmppConnection.this.disco) {
2577 ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid());
2578 return info != null && info.hasIdentity("pubsub", "pep");
2579 }
2580 }
2581
2582 public boolean pepPersistent() {
2583 synchronized (XmppConnection.this.disco) {
2584 ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid());
2585 return info != null
2586 && info.getFeatures()
2587 .contains("http://jabber.org/protocol/pubsub#persistent-items");
2588 }
2589 }
2590
2591 public boolean pepPublishOptions() {
2592 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUBSUB_PUBLISH_OPTIONS);
2593 }
2594
2595 public boolean pepOmemoWhitelisted() {
2596 return hasDiscoFeature(
2597 account.getJid().asBareJid(), AxolotlService.PEP_OMEMO_WHITELISTED);
2598 }
2599
2600 public boolean mam() {
2601 return MessageArchiveService.Version.has(getAccountFeatures());
2602 }
2603
2604 public List<String> getAccountFeatures() {
2605 ServiceDiscoveryResult result = connection.disco.get(account.getJid().asBareJid());
2606 return result == null ? Collections.emptyList() : result.getFeatures();
2607 }
2608
2609 public boolean push() {
2610 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUSH)
2611 || hasDiscoFeature(account.getDomain(), Namespace.PUSH);
2612 }
2613
2614 public boolean rosterVersioning() {
2615 return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver");
2616 }
2617
2618 public void setBlockListRequested(boolean value) {
2619 this.blockListRequested = value;
2620 }
2621
2622 public boolean httpUpload(long filesize) {
2623 if (Config.DISABLE_HTTP_UPLOAD) {
2624 return false;
2625 } else {
2626 for (String namespace :
2627 new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) {
2628 List<Entry<Jid, ServiceDiscoveryResult>> items =
2629 findDiscoItemsByFeature(namespace);
2630 if (items.size() > 0) {
2631 try {
2632 long maxsize =
2633 Long.parseLong(
2634 items.get(0)
2635 .getValue()
2636 .getExtendedDiscoInformation(
2637 namespace, "max-file-size"));
2638 if (filesize <= maxsize) {
2639 return true;
2640 } else {
2641 Log.d(
2642 Config.LOGTAG,
2643 account.getJid().asBareJid()
2644 + ": http upload is not available for files with size "
2645 + filesize
2646 + " (max is "
2647 + maxsize
2648 + ")");
2649 return false;
2650 }
2651 } catch (Exception e) {
2652 return true;
2653 }
2654 }
2655 }
2656 return false;
2657 }
2658 }
2659
2660 public boolean useLegacyHttpUpload() {
2661 return findDiscoItemByFeature(Namespace.HTTP_UPLOAD) == null
2662 && findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY) != null;
2663 }
2664
2665 public long getMaxHttpUploadSize() {
2666 for (String namespace :
2667 new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) {
2668 List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(namespace);
2669 if (items.size() > 0) {
2670 try {
2671 return Long.parseLong(
2672 items.get(0)
2673 .getValue()
2674 .getExtendedDiscoInformation(namespace, "max-file-size"));
2675 } catch (Exception e) {
2676 // ignored
2677 }
2678 }
2679 }
2680 return -1;
2681 }
2682
2683 public boolean stanzaIds() {
2684 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.STANZA_IDS);
2685 }
2686
2687 public boolean bookmarks2() {
2688 return Config
2689 .USE_BOOKMARKS2 /* || hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT)*/;
2690 }
2691
2692 public boolean externalServiceDiscovery() {
2693 return hasDiscoFeature(account.getDomain(), Namespace.EXTERNAL_SERVICE_DISCOVERY);
2694 }
2695 }
2696}