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