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