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