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