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