1package eu.siacs.conversations.xmpp;
2
3import android.graphics.Bitmap;
4import android.graphics.BitmapFactory;
5import android.os.Bundle;
6import android.os.Parcelable;
7import android.os.PowerManager;
8import android.os.PowerManager.WakeLock;
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 org.json.JSONException;
17import org.json.JSONObject;
18import org.xmlpull.v1.XmlPullParserException;
19
20import java.io.ByteArrayInputStream;
21import java.io.IOException;
22import java.io.InputStream;
23import java.math.BigInteger;
24import java.net.ConnectException;
25import java.net.IDN;
26import java.net.InetAddress;
27import java.net.InetSocketAddress;
28import java.net.Socket;
29import java.net.UnknownHostException;
30import java.net.URL;
31import java.security.KeyManagementException;
32import java.security.NoSuchAlgorithmException;
33import java.security.Principal;
34import java.security.PrivateKey;
35import java.security.cert.X509Certificate;
36import java.util.ArrayList;
37import java.util.HashMap;
38import java.util.Hashtable;
39import java.util.Iterator;
40import java.util.List;
41import java.util.Map.Entry;
42import java.util.concurrent.atomic.AtomicBoolean;
43import java.util.concurrent.atomic.AtomicInteger;
44
45import javax.net.ssl.HostnameVerifier;
46import javax.net.ssl.KeyManager;
47import javax.net.ssl.SSLContext;
48import javax.net.ssl.SSLSocket;
49import javax.net.ssl.SSLSocketFactory;
50import javax.net.ssl.X509KeyManager;
51import javax.net.ssl.X509TrustManager;
52
53import de.duenndns.ssl.MemorizingTrustManager;
54import eu.siacs.conversations.Config;
55import eu.siacs.conversations.crypto.XmppDomainVerifier;
56import eu.siacs.conversations.crypto.sasl.DigestMd5;
57import eu.siacs.conversations.crypto.sasl.External;
58import eu.siacs.conversations.crypto.sasl.Plain;
59import eu.siacs.conversations.crypto.sasl.SaslMechanism;
60import eu.siacs.conversations.crypto.sasl.ScramSha1;
61import eu.siacs.conversations.entities.Account;
62import eu.siacs.conversations.entities.Message;
63import eu.siacs.conversations.entities.ServiceDiscoveryResult;
64import eu.siacs.conversations.generator.IqGenerator;
65import eu.siacs.conversations.services.XmppConnectionService;
66import eu.siacs.conversations.utils.DNSHelper;
67import eu.siacs.conversations.utils.SSLSocketHelper;
68import eu.siacs.conversations.utils.SocksSocketFactory;
69import eu.siacs.conversations.utils.Xmlns;
70import eu.siacs.conversations.xml.Element;
71import eu.siacs.conversations.xml.Tag;
72import eu.siacs.conversations.xml.TagWriter;
73import eu.siacs.conversations.xml.XmlReader;
74import eu.siacs.conversations.xmpp.forms.Data;
75import eu.siacs.conversations.xmpp.forms.Field;
76import eu.siacs.conversations.xmpp.jid.InvalidJidException;
77import eu.siacs.conversations.xmpp.jid.Jid;
78import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
79import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
80import eu.siacs.conversations.xmpp.stanzas.AbstractAcknowledgeableStanza;
81import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
82import eu.siacs.conversations.xmpp.stanzas.IqPacket;
83import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
84import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
85import eu.siacs.conversations.xmpp.stanzas.csi.ActivePacket;
86import eu.siacs.conversations.xmpp.stanzas.csi.InactivePacket;
87import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
88import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
89import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
90import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
91
92public class XmppConnection implements Runnable {
93
94 private static final int PACKET_IQ = 0;
95 private static final int PACKET_MESSAGE = 1;
96 private static final int PACKET_PRESENCE = 2;
97 protected Account account;
98 private final WakeLock wakeLock;
99 private Socket socket;
100 private XmlReader tagReader;
101 private TagWriter tagWriter;
102 private final Features features = new Features(this);
103 private boolean needsBinding = true;
104 private boolean shouldAuthenticate = true;
105 private Element streamFeatures;
106 private final HashMap<Jid, ServiceDiscoveryResult> disco = new HashMap<>();
107
108 private String streamId = null;
109 private int smVersion = 3;
110 private final SparseArray<AbstractAcknowledgeableStanza> mStanzaQueue = new SparseArray<>();
111
112 private int stanzasReceived = 0;
113 private int stanzasSent = 0;
114 private long lastPacketReceived = 0;
115 private long lastPingSent = 0;
116 private long lastConnect = 0;
117 private long lastSessionStarted = 0;
118 private long lastDiscoStarted = 0;
119 private AtomicInteger mPendingServiceDiscoveries = new AtomicInteger(0);
120 private AtomicBoolean mWaitForDisco = new AtomicBoolean(true);
121 private boolean mInteractive = false;
122 private int attempt = 0;
123 private final Hashtable<String, Pair<IqPacket, OnIqPacketReceived>> packetCallbacks = new Hashtable<>();
124 private OnPresencePacketReceived presenceListener = null;
125 private OnJinglePacketReceived jingleListener = null;
126 private OnIqPacketReceived unregisteredIqListener = null;
127 private OnMessagePacketReceived messageListener = null;
128 private OnStatusChanged statusListener = null;
129 private OnBindListener bindListener = null;
130 private final ArrayList<OnAdvancedStreamFeaturesLoaded> advancedStreamFeaturesLoadedListeners = new ArrayList<>();
131 private OnMessageAcknowledged acknowledgedListener = null;
132 private XmppConnectionService mXmppConnectionService = null;
133
134 private SaslMechanism saslMechanism;
135
136 private X509KeyManager mKeyManager = new X509KeyManager() {
137 @Override
138 public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
139 return account.getPrivateKeyAlias();
140 }
141
142 @Override
143 public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
144 return null;
145 }
146
147 @Override
148 public X509Certificate[] getCertificateChain(String alias) {
149 try {
150 return KeyChain.getCertificateChain(mXmppConnectionService, alias);
151 } catch (Exception e) {
152 return new X509Certificate[0];
153 }
154 }
155
156 @Override
157 public String[] getClientAliases(String s, Principal[] principals) {
158 return new String[0];
159 }
160
161 @Override
162 public String[] getServerAliases(String s, Principal[] principals) {
163 return new String[0];
164 }
165
166 @Override
167 public PrivateKey getPrivateKey(String alias) {
168 try {
169 return KeyChain.getPrivateKey(mXmppConnectionService, alias);
170 } catch (Exception e) {
171 return null;
172 }
173 }
174 };
175 private Identity mServerIdentity = Identity.UNKNOWN;
176
177 public final OnIqPacketReceived registrationResponseListener = new OnIqPacketReceived() {
178 @Override
179 public void onIqPacketReceived(Account account, IqPacket packet) {
180 if (packet.getType() == IqPacket.TYPE.RESULT) {
181 account.setOption(Account.OPTION_REGISTER, false);
182 forceCloseSocket();
183 changeStatus(Account.State.REGISTRATION_SUCCESSFUL);
184 } else {
185 Element error = packet.findChild("error");
186 if (error != null && error.hasChild("conflict")) {
187 forceCloseSocket();
188 changeStatus(Account.State.REGISTRATION_CONFLICT);
189 } else if (error != null
190 && "wait".equals(error.getAttribute("type"))
191 && error.hasChild("resource-constraint")) {
192 forceCloseSocket();
193 changeStatus(Account.State.REGISTRATION_PLEASE_WAIT);
194 } else {
195 forceCloseSocket();
196 changeStatus(Account.State.REGISTRATION_FAILED);
197 Log.d(Config.LOGTAG, packet.toString());
198 }
199 }
200 }
201 };
202
203 public XmppConnection(final Account account, final XmppConnectionService service) {
204 this.account = account;
205 this.wakeLock = service.getPowerManager().newWakeLock(
206 PowerManager.PARTIAL_WAKE_LOCK, account.getJid().toBareJid().toString());
207 tagWriter = new TagWriter();
208 mXmppConnectionService = service;
209 }
210
211 protected void changeStatus(final Account.State nextStatus) {
212 if (account.getStatus() != nextStatus) {
213 if ((nextStatus == Account.State.OFFLINE)
214 && (account.getStatus() != Account.State.CONNECTING)
215 && (account.getStatus() != Account.State.ONLINE)
216 && (account.getStatus() != Account.State.DISABLED)) {
217 return;
218 }
219 if (nextStatus == Account.State.ONLINE) {
220 this.attempt = 0;
221 }
222 account.setStatus(nextStatus);
223 if (statusListener != null) {
224 statusListener.onStatusChanged(account);
225 }
226 }
227 }
228
229 public void prepareNewConnection() {
230 this.lastConnect = SystemClock.elapsedRealtime();
231 this.lastPingSent = SystemClock.elapsedRealtime();
232 this.lastDiscoStarted = Long.MAX_VALUE;
233 this.changeStatus(Account.State.CONNECTING);
234 }
235
236 protected void connect() {
237 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": connecting");
238 features.encryptionEnabled = false;
239 this.attempt++;
240 switch (account.getJid().getDomainpart()) {
241 case "chat.facebook.com":
242 mServerIdentity = Identity.FACEBOOK;
243 break;
244 case "nimbuzz.com":
245 mServerIdentity = Identity.NIMBUZZ;
246 break;
247 default:
248 mServerIdentity = Identity.UNKNOWN;
249 break;
250 }
251 try {
252 shouldAuthenticate = needsBinding = !account.isOptionSet(Account.OPTION_REGISTER);
253 tagReader = new XmlReader(wakeLock);
254 tagWriter = new TagWriter();
255 this.changeStatus(Account.State.CONNECTING);
256 final boolean useTor = mXmppConnectionService.useTorToConnect() || account.isOnion();
257 final boolean extended = mXmppConnectionService.showExtendedConnectionOptions();
258 if (useTor) {
259 String destination;
260 if (account.getHostname() == null || account.getHostname().isEmpty()) {
261 destination = account.getServer().toString();
262 } else {
263 destination = account.getHostname();
264 }
265 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": connect to " + destination + " via TOR");
266 socket = SocksSocketFactory.createSocketOverTor(destination, account.getPort());
267 startXmpp();
268 } else if (extended && account.getHostname() != null && !account.getHostname().isEmpty()) {
269 socket = new Socket();
270 try {
271 socket.connect(new InetSocketAddress(account.getHostname(), account.getPort()), Config.SOCKET_TIMEOUT * 1000);
272 } catch (IOException e) {
273 throw new UnknownHostException();
274 }
275 startXmpp();
276 } else if (DNSHelper.isIp(account.getServer().toString())) {
277 socket = new Socket();
278 try {
279 socket.connect(new InetSocketAddress(account.getServer().toString(), 5222), Config.SOCKET_TIMEOUT * 1000);
280 } catch (IOException e) {
281 throw new UnknownHostException();
282 }
283 startXmpp();
284 } else {
285 final Bundle result = DNSHelper.getSRVRecord(account.getServer(), mXmppConnectionService);
286 final ArrayList<Parcelable>values = result.getParcelableArrayList("values");
287 for(Iterator<Parcelable> iterator = values.iterator(); iterator.hasNext();) {
288 if (Thread.currentThread().isInterrupted()) {
289 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": Thread was interrupted");
290 return;
291 }
292 final Bundle namePort = (Bundle) iterator.next();
293 try {
294 String srvRecordServer;
295 try {
296 srvRecordServer = IDN.toASCII(namePort.getString("name"));
297 } catch (final IllegalArgumentException e) {
298 // TODO: Handle me?`
299 srvRecordServer = "";
300 }
301 final int srvRecordPort = namePort.getInt("port");
302 final String srvIpServer = namePort.getString("ip");
303 // if tls is true, encryption is implied and must not be started
304 features.encryptionEnabled = namePort.getBoolean("tls");
305 final InetSocketAddress addr;
306 if (srvIpServer != null) {
307 addr = new InetSocketAddress(srvIpServer, srvRecordPort);
308 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
309 + ": using values from dns " + srvRecordServer
310 + "[" + srvIpServer + "]:" + srvRecordPort + " tls: " + features.encryptionEnabled);
311 } else {
312 addr = new InetSocketAddress(srvRecordServer, srvRecordPort);
313 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
314 + ": using values from dns "
315 + srvRecordServer + ":" + srvRecordPort + " tls: " + features.encryptionEnabled);
316 }
317
318 if (!features.encryptionEnabled) {
319 socket = new Socket();
320 socket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
321 } else {
322 final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier();
323 socket = tlsFactoryVerifier.factory.createSocket();
324
325 if (socket == null) {
326 throw new IOException("could not initialize ssl socket");
327 }
328
329 SSLSocketHelper.setSecurity((SSLSocket) socket);
330 SSLSocketHelper.setSNIHost(tlsFactoryVerifier.factory, (SSLSocket) socket, account.getServer().getDomainpart());
331 SSLSocketHelper.setAlpnProtocol(tlsFactoryVerifier.factory, (SSLSocket) socket, "xmpp-client");
332
333 socket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
334
335 if (!tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), ((SSLSocket) socket).getSession())) {
336 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed");
337 throw new SecurityException();
338 }
339 }
340
341 if (startXmpp())
342 break; // successfully connected to server that speaks xmpp
343 } catch(final SecurityException e) {
344 throw e;
345 } catch (final Throwable e) {
346 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage() +"("+e.getClass().getName()+")");
347 if (!iterator.hasNext()) {
348 throw new UnknownHostException();
349 }
350 }
351 }
352 }
353 processStream();
354 } catch (final IncompatibleServerException e) {
355 this.changeStatus(Account.State.INCOMPATIBLE_SERVER);
356 } catch (final SecurityException e) {
357 this.changeStatus(Account.State.SECURITY_ERROR);
358 } catch (final UnauthorizedException e) {
359 this.changeStatus(Account.State.UNAUTHORIZED);
360 } catch (final UnknownHostException | ConnectException e) {
361 this.changeStatus(Account.State.SERVER_NOT_FOUND);
362 } catch (final SocksSocketFactory.SocksProxyNotFoundException e) {
363 this.changeStatus(Account.State.TOR_NOT_AVAILABLE);
364 } catch(final StreamErrorHostUnknown e) {
365 this.changeStatus(Account.State.HOST_UNKNOWN);
366 } catch(final StreamErrorPolicyViolation e) {
367 this.changeStatus(Account.State.POLICY_VIOLATION);
368 } catch(final StreamError e) {
369 this.changeStatus(Account.State.STREAM_ERROR);
370 } catch (final IOException | XmlPullParserException | NoSuchAlgorithmException e) {
371 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage());
372 this.changeStatus(Account.State.OFFLINE);
373 this.attempt = Math.max(0, this.attempt - 1);
374 } finally {
375 forceCloseSocket();
376 if (wakeLock.isHeld()) {
377 try {
378 wakeLock.release();
379 } catch (final RuntimeException ignored) {
380 }
381 }
382 }
383 }
384
385 /**
386 * Starts xmpp protocol, call after connecting to socket
387 * @return true if server returns with valid xmpp, false otherwise
388 * @throws IOException Unknown tag on connect
389 * @throws XmlPullParserException Bad Xml
390 * @throws NoSuchAlgorithmException Other error
391 */
392 private boolean startXmpp() throws IOException, XmlPullParserException, NoSuchAlgorithmException {
393 tagWriter.setOutputStream(socket.getOutputStream());
394 tagReader.setInputStream(socket.getInputStream());
395 tagWriter.beginDocument();
396 sendStartStream();
397 Tag nextTag;
398 while ((nextTag = tagReader.readTag()) != null) {
399 if (nextTag.isStart("stream")) {
400 return true;
401 } else {
402 throw new IOException("unknown tag on connect");
403 }
404 }
405 if (socket.isConnected()) {
406 socket.close();
407 }
408 return false;
409 }
410
411 private static class TlsFactoryVerifier {
412 private final SSLSocketFactory factory;
413 private final HostnameVerifier verifier;
414
415 public TlsFactoryVerifier(final SSLSocketFactory factory, final HostnameVerifier verifier) throws IOException {
416 this.factory = factory;
417 this.verifier = verifier;
418 if (factory == null || verifier == null) {
419 throw new IOException("could not setup ssl");
420 }
421 }
422 }
423
424 private TlsFactoryVerifier getTlsFactoryVerifier() throws NoSuchAlgorithmException, KeyManagementException, IOException {
425 final SSLContext sc = SSLSocketHelper.getSSLContext();
426 MemorizingTrustManager trustManager = this.mXmppConnectionService.getMemorizingTrustManager();
427 KeyManager[] keyManager;
428 if (account.getPrivateKeyAlias() != null && account.getPassword().isEmpty()) {
429 keyManager = new KeyManager[]{mKeyManager};
430 } else {
431 keyManager = null;
432 }
433 sc.init(keyManager, new X509TrustManager[]{mInteractive ? trustManager : trustManager.getNonInteractive()}, mXmppConnectionService.getRNG());
434 final SSLSocketFactory factory = sc.getSocketFactory();
435 final HostnameVerifier verifier;
436 if (mInteractive) {
437 verifier = trustManager.wrapHostnameVerifier(new XmppDomainVerifier());
438 } else {
439 verifier = trustManager.wrapHostnameVerifierNonInteractive(new XmppDomainVerifier());
440 }
441
442 return new TlsFactoryVerifier(factory, verifier);
443 }
444
445 @Override
446 public void run() {
447 forceCloseSocket();
448 connect();
449 }
450
451 private void processStream() throws XmlPullParserException, IOException, NoSuchAlgorithmException {
452 Tag nextTag = tagReader.readTag();
453 while (nextTag != null && !nextTag.isEnd("stream")) {
454 if (nextTag.isStart("error")) {
455 processStreamError(nextTag);
456 } else if (nextTag.isStart("features")) {
457 processStreamFeatures(nextTag);
458 } else if (nextTag.isStart("proceed")) {
459 switchOverToTls(nextTag);
460 } else if (nextTag.isStart("success")) {
461 final String challenge = tagReader.readElement(nextTag).getContent();
462 try {
463 saslMechanism.getResponse(challenge);
464 } catch (final SaslMechanism.AuthenticationException e) {
465 disconnect(true);
466 Log.e(Config.LOGTAG, String.valueOf(e));
467 }
468 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": logged in");
469 account.setKey(Account.PINNED_MECHANISM_KEY,
470 String.valueOf(saslMechanism.getPriority()));
471 tagReader.reset();
472 sendStartStream();
473 final Tag tag = tagReader.readTag();
474 if (tag != null && tag.isStart("stream")) {
475 processStream();
476 } else {
477 throw new IOException("server didn't restart stream after successful auth");
478 }
479 break;
480 } else if (nextTag.isStart("failure")) {
481 throw new UnauthorizedException();
482 } else if (nextTag.isStart("challenge")) {
483 final String challenge = tagReader.readElement(nextTag).getContent();
484 final Element response = new Element("response");
485 response.setAttribute("xmlns",
486 "urn:ietf:params:xml:ns:xmpp-sasl");
487 try {
488 response.setContent(saslMechanism.getResponse(challenge));
489 } catch (final SaslMechanism.AuthenticationException e) {
490 // TODO: Send auth abort tag.
491 Log.e(Config.LOGTAG, e.toString());
492 }
493 tagWriter.writeElement(response);
494 } else if (nextTag.isStart("enabled")) {
495 final Element enabled = tagReader.readElement(nextTag);
496 if ("true".equals(enabled.getAttribute("resume"))) {
497 this.streamId = enabled.getAttribute("id");
498 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
499 + ": stream management(" + smVersion
500 + ") enabled (resumable)");
501 } else {
502 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
503 + ": stream management(" + smVersion + ") enabled");
504 }
505 this.stanzasReceived = 0;
506 final RequestPacket r = new RequestPacket(smVersion);
507 tagWriter.writeStanzaAsync(r);
508 } else if (nextTag.isStart("resumed")) {
509 lastPacketReceived = SystemClock.elapsedRealtime();
510 final Element resumed = tagReader.readElement(nextTag);
511 final String h = resumed.getAttribute("h");
512 try {
513 ArrayList<AbstractAcknowledgeableStanza> failedStanzas = new ArrayList<>();
514 synchronized (this.mStanzaQueue) {
515 final int serverCount = Integer.parseInt(h);
516 if (serverCount != stanzasSent) {
517 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
518 + ": session resumed with lost packages");
519 stanzasSent = serverCount;
520 } else {
521 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": session resumed");
522 }
523 acknowledgeStanzaUpTo(serverCount);
524 for (int i = 0; i < this.mStanzaQueue.size(); ++i) {
525 failedStanzas.add(mStanzaQueue.valueAt(i));
526 }
527 mStanzaQueue.clear();
528 }
529 Log.d(Config.LOGTAG, "resending " + failedStanzas.size() + " stanzas");
530 for (AbstractAcknowledgeableStanza packet : failedStanzas) {
531 if (packet instanceof MessagePacket) {
532 MessagePacket message = (MessagePacket) packet;
533 mXmppConnectionService.markMessage(account,
534 message.getTo().toBareJid(),
535 message.getId(),
536 Message.STATUS_UNSEND);
537 }
538 sendPacket(packet);
539 }
540 } catch (final NumberFormatException ignored) {
541 }
542 Log.d(Config.LOGTAG, account.getJid().toBareJid()+ ": online with resource " + account.getResource());
543 changeStatus(Account.State.ONLINE);
544 } else if (nextTag.isStart("r")) {
545 tagReader.readElement(nextTag);
546 if (Config.EXTENDED_SM_LOGGING) {
547 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": acknowledging stanza #" + this.stanzasReceived);
548 }
549 final AckPacket ack = new AckPacket(this.stanzasReceived, smVersion);
550 tagWriter.writeStanzaAsync(ack);
551 } else if (nextTag.isStart("a")) {
552 final Element ack = tagReader.readElement(nextTag);
553 lastPacketReceived = SystemClock.elapsedRealtime();
554 try {
555 synchronized (this.mStanzaQueue) {
556 final int serverSequence = Integer.parseInt(ack.getAttribute("h"));
557 acknowledgeStanzaUpTo(serverSequence);
558 }
559 } catch (NumberFormatException | NullPointerException e) {
560 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": server send ack without sequence number");
561 }
562 } else if (nextTag.isStart("failed")) {
563 Element failed = tagReader.readElement(nextTag);
564 try {
565 final int serverCount = Integer.parseInt(failed.getAttribute("h"));
566 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": resumption failed but server acknowledged stanza #"+serverCount);
567 synchronized (this.mStanzaQueue) {
568 acknowledgeStanzaUpTo(serverCount);
569 }
570 } catch (NumberFormatException | NullPointerException e) {
571 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": resumption failed");
572 }
573 resetStreamId();
574 if (account.getStatus() != Account.State.ONLINE) {
575 sendBindRequest();
576 }
577 } else if (nextTag.isStart("iq")) {
578 processIq(nextTag);
579 } else if (nextTag.isStart("message")) {
580 processMessage(nextTag);
581 } else if (nextTag.isStart("presence")) {
582 processPresence(nextTag);
583 }
584 nextTag = tagReader.readTag();
585 }
586 }
587
588 private void acknowledgeStanzaUpTo(int serverCount) {
589 for (int i = 0; i < mStanzaQueue.size(); ++i) {
590 if (serverCount >= mStanzaQueue.keyAt(i)) {
591 if (Config.EXTENDED_SM_LOGGING) {
592 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": server acknowledged stanza #" + mStanzaQueue.keyAt(i));
593 }
594 AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i);
595 if (stanza instanceof MessagePacket && acknowledgedListener != null) {
596 MessagePacket packet = (MessagePacket) stanza;
597 acknowledgedListener.onMessageAcknowledged(account, packet.getId());
598 }
599 mStanzaQueue.removeAt(i);
600 i--;
601 }
602 }
603 }
604
605 private Element processPacket(final Tag currentTag, final int packetType)
606 throws XmlPullParserException, IOException {
607 Element element;
608 switch (packetType) {
609 case PACKET_IQ:
610 element = new IqPacket();
611 break;
612 case PACKET_MESSAGE:
613 element = new MessagePacket();
614 break;
615 case PACKET_PRESENCE:
616 element = new PresencePacket();
617 break;
618 default:
619 return null;
620 }
621 element.setAttributes(currentTag.getAttributes());
622 Tag nextTag = tagReader.readTag();
623 if (nextTag == null) {
624 throw new IOException("interrupted mid tag");
625 }
626 while (!nextTag.isEnd(element.getName())) {
627 if (!nextTag.isNo()) {
628 final Element child = tagReader.readElement(nextTag);
629 final String type = currentTag.getAttribute("type");
630 if (packetType == PACKET_IQ
631 && "jingle".equals(child.getName())
632 && ("set".equalsIgnoreCase(type) || "get"
633 .equalsIgnoreCase(type))) {
634 element = new JinglePacket();
635 element.setAttributes(currentTag.getAttributes());
636 }
637 element.addChild(child);
638 }
639 nextTag = tagReader.readTag();
640 if (nextTag == null) {
641 throw new IOException("interrupted mid tag");
642 }
643 }
644 if (stanzasReceived == Integer.MAX_VALUE) {
645 resetStreamId();
646 throw new IOException("time to restart the session. cant handle >2 billion pcks");
647 }
648 ++stanzasReceived;
649 lastPacketReceived = SystemClock.elapsedRealtime();
650 if (Config.BACKGROUND_STANZA_LOGGING && mXmppConnectionService.checkListeners()) {
651 Log.d(Config.LOGTAG,"[background stanza] "+element);
652 }
653 return element;
654 }
655
656 private void processIq(final Tag currentTag) throws XmlPullParserException, IOException {
657 final IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
658
659 if (packet.getId() == null) {
660 return; // an iq packet without id is definitely invalid
661 }
662
663 if (packet instanceof JinglePacket) {
664 if (this.jingleListener != null) {
665 this.jingleListener.onJinglePacketReceived(account,(JinglePacket) packet);
666 }
667 } else {
668 OnIqPacketReceived callback = null;
669 synchronized (this.packetCallbacks) {
670 if (packetCallbacks.containsKey(packet.getId())) {
671 final Pair<IqPacket, OnIqPacketReceived> packetCallbackDuple = packetCallbacks.get(packet.getId());
672 // Packets to the server should have responses from the server
673 if (packetCallbackDuple.first.toServer(account)) {
674 if (packet.fromServer(account) || mServerIdentity == Identity.FACEBOOK) {
675 callback = packetCallbackDuple.second;
676 packetCallbacks.remove(packet.getId());
677 } else {
678 Log.e(Config.LOGTAG, account.getJid().toBareJid().toString() + ": ignoring spoofed iq packet");
679 }
680 } else {
681 if (packet.getFrom().equals(packetCallbackDuple.first.getTo())) {
682 callback = packetCallbackDuple.second;
683 packetCallbacks.remove(packet.getId());
684 } else {
685 Log.e(Config.LOGTAG, account.getJid().toBareJid().toString() + ": ignoring spoofed iq packet");
686 }
687 }
688 } else if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) {
689 callback = this.unregisteredIqListener;
690 }
691 }
692 if (callback != null) {
693 callback.onIqPacketReceived(account,packet);
694 }
695 }
696 }
697
698 private void processMessage(final Tag currentTag) throws XmlPullParserException, IOException {
699 final MessagePacket packet = (MessagePacket) processPacket(currentTag,PACKET_MESSAGE);
700 this.messageListener.onMessagePacketReceived(account, packet);
701 }
702
703 private void processPresence(final Tag currentTag) throws XmlPullParserException, IOException {
704 PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE);
705 this.presenceListener.onPresencePacketReceived(account, packet);
706 }
707
708 private void sendStartTLS() throws IOException {
709 final Tag startTLS = Tag.empty("starttls");
710 startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
711 tagWriter.writeTag(startTLS);
712 }
713
714
715
716 private void switchOverToTls(final Tag currentTag) throws XmlPullParserException, IOException {
717 tagReader.readTag();
718 try {
719 final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier();
720 final InetAddress address = socket == null ? null : socket.getInetAddress();
721
722 if (address == null) {
723 throw new IOException("could not setup ssl");
724 }
725
726 final SSLSocket sslSocket = (SSLSocket) tlsFactoryVerifier.factory.createSocket(socket, address.getHostAddress(), socket.getPort(), true);
727
728 if (sslSocket == null) {
729 throw new IOException("could not initialize ssl socket");
730 }
731
732 SSLSocketHelper.setSecurity(sslSocket);
733
734 if (!tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), sslSocket.getSession())) {
735 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": TLS certificate verification failed");
736 throw new SecurityException();
737 }
738 tagReader.setInputStream(sslSocket.getInputStream());
739 tagWriter.setOutputStream(sslSocket.getOutputStream());
740 sendStartStream();
741 Log.d(Config.LOGTAG, account.getJid().toBareJid()+ ": TLS connection established");
742 features.encryptionEnabled = true;
743 final Tag tag = tagReader.readTag();
744 if (tag != null && tag.isStart("stream")) {
745 processStream();
746 } else {
747 throw new IOException("server didn't restart stream after STARTTLS");
748 }
749 sslSocket.close();
750 } catch (final NoSuchAlgorithmException | KeyManagementException e1) {
751 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed");
752 throw new SecurityException();
753 }
754 }
755
756 private void processStreamFeatures(final Tag currentTag)
757 throws XmlPullParserException, IOException {
758 this.streamFeatures = tagReader.readElement(currentTag);
759 if (this.streamFeatures.hasChild("starttls") && !features.encryptionEnabled) {
760 sendStartTLS();
761 } else if (this.streamFeatures.hasChild("register") && account.isOptionSet(Account.OPTION_REGISTER)) {
762 if (features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS) {
763 sendRegistryRequest();
764 } else {
765 throw new IncompatibleServerException();
766 }
767 } else if (!this.streamFeatures.hasChild("register")
768 && account.isOptionSet(Account.OPTION_REGISTER)) {
769 forceCloseSocket();
770 changeStatus(Account.State.REGISTRATION_NOT_SUPPORTED);
771 } else if (this.streamFeatures.hasChild("mechanisms")
772 && shouldAuthenticate
773 && (features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS)) {
774 authenticate();
775 } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" + smVersion) && streamId != null) {
776 if (Config.EXTENDED_SM_LOGGING) {
777 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": resuming after stanza #"+stanzasReceived);
778 }
779 final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived, smVersion);
780 this.tagWriter.writeStanzaAsync(resume);
781 } else if (needsBinding) {
782 if (this.streamFeatures.hasChild("bind")) {
783 sendBindRequest();
784 } else {
785 throw new IncompatibleServerException();
786 }
787 }
788 }
789
790 private void authenticate() throws IOException {
791 final List<String> mechanisms = extractMechanisms(streamFeatures
792 .findChild("mechanisms"));
793 final Element auth = new Element("auth");
794 auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
795 if (mechanisms.contains("EXTERNAL") && account.getPrivateKeyAlias() != null) {
796 saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG());
797 } else if (mechanisms.contains("SCRAM-SHA-1")) {
798 saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG());
799 } else if (mechanisms.contains("PLAIN")) {
800 saslMechanism = new Plain(tagWriter, account);
801 } else if (mechanisms.contains("DIGEST-MD5")) {
802 saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG());
803 }
804 if (saslMechanism != null) {
805 final JSONObject keys = account.getKeys();
806 try {
807 if (keys.has(Account.PINNED_MECHANISM_KEY) &&
808 keys.getInt(Account.PINNED_MECHANISM_KEY) > saslMechanism.getPriority()) {
809 Log.e(Config.LOGTAG, "Auth failed. Authentication mechanism " + saslMechanism.getMechanism() +
810 " has lower priority (" + String.valueOf(saslMechanism.getPriority()) +
811 ") than pinned priority (" + keys.getInt(Account.PINNED_MECHANISM_KEY) +
812 "). Possible downgrade attack?");
813 throw new SecurityException();
814 }
815 } catch (final JSONException e) {
816 Log.d(Config.LOGTAG, "Parse error while checking pinned auth mechanism");
817 }
818 Log.d(Config.LOGTAG, account.getJid().toString() + ": Authenticating with " + saslMechanism.getMechanism());
819 auth.setAttribute("mechanism", saslMechanism.getMechanism());
820 if (!saslMechanism.getClientFirstMessage().isEmpty()) {
821 auth.setContent(saslMechanism.getClientFirstMessage());
822 }
823 tagWriter.writeElement(auth);
824 } else {
825 throw new IncompatibleServerException();
826 }
827 }
828
829 private List<String> extractMechanisms(final Element stream) {
830 final ArrayList<String> mechanisms = new ArrayList<>(stream
831 .getChildren().size());
832 for (final Element child : stream.getChildren()) {
833 mechanisms.add(child.getContent());
834 }
835 return mechanisms;
836 }
837
838 private void sendRegistryRequest() {
839 final IqPacket register = new IqPacket(IqPacket.TYPE.GET);
840 register.query("jabber:iq:register");
841 register.setTo(account.getServer());
842 sendIqPacket(register, new OnIqPacketReceived() {
843
844 @Override
845 public void onIqPacketReceived(final Account account, final IqPacket packet) {
846 boolean failed = false;
847 if (packet.getType() == IqPacket.TYPE.RESULT
848 && packet.query().hasChild("username")
849 && (packet.query().hasChild("password"))) {
850 final IqPacket register = new IqPacket(IqPacket.TYPE.SET);
851 final Element username = new Element("username").setContent(account.getUsername());
852 final Element password = new Element("password").setContent(account.getPassword());
853 register.query("jabber:iq:register").addChild(username);
854 register.query().addChild(password);
855 sendIqPacket(register, registrationResponseListener);
856 } else if (packet.getType() == IqPacket.TYPE.RESULT
857 && (packet.query().hasChild("x", "jabber:x:data"))) {
858 final Data data = Data.parse(packet.query().findChild("x", "jabber:x:data"));
859 final Element blob = packet.query().findChild("data", "urn:xmpp:bob");
860 final String id = packet.getId();
861
862 Bitmap captcha = null;
863 if (blob != null) {
864 try {
865 final String base64Blob = blob.getContent();
866 final byte[] strBlob = Base64.decode(base64Blob, Base64.DEFAULT);
867 InputStream stream = new ByteArrayInputStream(strBlob);
868 captcha = BitmapFactory.decodeStream(stream);
869 } catch (Exception e) {
870 //ignored
871 }
872 } else {
873 try {
874 Field url = data.getFieldByName("url");
875 String urlString = url.findChildContent("value");
876 URL uri = new URL(urlString);
877 captcha = BitmapFactory.decodeStream(uri.openConnection().getInputStream());
878 } catch (IOException e) {
879 Log.e(Config.LOGTAG, e.toString());
880 }
881 }
882
883 if (captcha != null) {
884 failed = !mXmppConnectionService.displayCaptchaRequest(account, id, data, captcha);
885 }
886 } else {
887 failed = true;
888 }
889
890 if (failed) {
891 final Element instructions = packet.query().findChild("instructions");
892 setAccountCreationFailed((instructions != null) ? instructions.getContent() : "");
893 }
894 }
895 });
896 }
897
898 private void setAccountCreationFailed(String instructions) {
899 changeStatus(Account.State.REGISTRATION_FAILED);
900 disconnect(true);
901 Log.d(Config.LOGTAG, account.getJid().toBareJid()
902 + ": could not register. instructions are"
903 + instructions);
904 }
905
906 public void resetEverything() {
907 resetAttemptCount();
908 resetStreamId();
909 clearIqCallbacks();
910 mStanzaQueue.clear();
911 synchronized (this.disco) {
912 disco.clear();
913 }
914 }
915
916 private void sendBindRequest() {
917 while(!mXmppConnectionService.areMessagesInitialized() && socket != null && !socket.isClosed()) {
918 try {
919 Thread.sleep(500);
920 } catch (final InterruptedException ignored) {
921 }
922 }
923 needsBinding = false;
924 clearIqCallbacks();
925 final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
926 iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind")
927 .addChild("resource").setContent(account.getResource());
928 this.sendUnmodifiedIqPacket(iq, new OnIqPacketReceived() {
929 @Override
930 public void onIqPacketReceived(final Account account, final IqPacket packet) {
931 if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
932 return;
933 }
934 final Element bind = packet.findChild("bind");
935 if (bind != null && packet.getType() == IqPacket.TYPE.RESULT) {
936 final Element jid = bind.findChild("jid");
937 if (jid != null && jid.getContent() != null) {
938 try {
939 account.setResource(Jid.fromString(jid.getContent()).getResourcepart());
940 if (streamFeatures.hasChild("session")
941 && !streamFeatures.findChild("session").hasChild("optional")) {
942 sendStartSession();
943 } else {
944 sendPostBindInitialization();
945 }
946 return;
947 } catch (final InvalidJidException e) {
948 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": server reported invalid jid ("+jid.getContent()+") on bind");
949 }
950 } else {
951 Log.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure. (no jid)");
952 }
953 } else {
954 Log.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure (" + packet.toString());
955 }
956 forceCloseSocket();
957 changeStatus(Account.State.BIND_FAILURE);
958 }
959 });
960 }
961
962 private void clearIqCallbacks() {
963 final IqPacket failurePacket = new IqPacket(IqPacket.TYPE.TIMEOUT);
964 final ArrayList<OnIqPacketReceived> callbacks = new ArrayList<>();
965 synchronized (this.packetCallbacks) {
966 if (this.packetCallbacks.size() == 0) {
967 return;
968 }
969 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": clearing "+this.packetCallbacks.size()+" iq callbacks");
970 final Iterator<Pair<IqPacket, OnIqPacketReceived>> iterator = this.packetCallbacks.values().iterator();
971 while (iterator.hasNext()) {
972 Pair<IqPacket, OnIqPacketReceived> entry = iterator.next();
973 callbacks.add(entry.second);
974 iterator.remove();
975 }
976 }
977 for(OnIqPacketReceived callback : callbacks) {
978 callback.onIqPacketReceived(account,failurePacket);
979 }
980 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": done clearing iq callbacks. " + this.packetCallbacks.size() + " left");
981 }
982
983 public void sendDiscoTimeout() {
984 if (mWaitForDisco.compareAndSet(true, false)) {
985 finalizeBind();
986 }
987 }
988
989 private void sendStartSession() {
990 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": sending legacy session to outdated server");
991 final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET);
992 startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session");
993 this.sendUnmodifiedIqPacket(startSession, new OnIqPacketReceived() {
994 @Override
995 public void onIqPacketReceived(Account account, IqPacket packet) {
996 if (packet.getType() == IqPacket.TYPE.RESULT) {
997 sendPostBindInitialization();
998 } else if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
999 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not init sessions");
1000 disconnect(true);
1001 }
1002 }
1003 });
1004 }
1005
1006 private void sendPostBindInitialization() {
1007 smVersion = 0;
1008 if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) {
1009 smVersion = 3;
1010 } else if (streamFeatures.hasChild("sm", "urn:xmpp:sm:2")) {
1011 smVersion = 2;
1012 }
1013 if (smVersion != 0) {
1014 synchronized (this.mStanzaQueue) {
1015 final EnablePacket enable = new EnablePacket(smVersion);
1016 tagWriter.writeStanzaAsync(enable);
1017 stanzasSent = 0;
1018 mStanzaQueue.clear();
1019 }
1020 }
1021 features.carbonsEnabled = false;
1022 features.blockListRequested = false;
1023 synchronized (this.disco) {
1024 this.disco.clear();
1025 }
1026 mPendingServiceDiscoveries.set(0);
1027 mWaitForDisco.set(mServerIdentity != Identity.NIMBUZZ);
1028 lastDiscoStarted = SystemClock.elapsedRealtime();
1029 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": starting service discovery");
1030 mXmppConnectionService.scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
1031 Element caps = streamFeatures.findChild("c");
1032 final String hash = caps == null ? null : caps.getAttribute("hash");
1033 final String ver = caps == null ? null : caps.getAttribute("ver");
1034 ServiceDiscoveryResult discoveryResult = null;
1035 if (hash != null && ver != null) {
1036 discoveryResult = mXmppConnectionService.getCachedServiceDiscoveryResult(new Pair<>(hash, ver));
1037 }
1038 if (discoveryResult == null) {
1039 sendServiceDiscoveryInfo(account.getServer());
1040 } else {
1041 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": server caps came from cache");
1042 disco.put(account.getServer(), discoveryResult);
1043 }
1044 sendServiceDiscoveryInfo(account.getJid().toBareJid());
1045 sendServiceDiscoveryItems(account.getServer());
1046
1047 if (!mWaitForDisco.get()) {
1048 finalizeBind();
1049 }
1050 this.lastSessionStarted = SystemClock.elapsedRealtime();
1051 }
1052
1053 private void sendServiceDiscoveryInfo(final Jid jid) {
1054 mPendingServiceDiscoveries.incrementAndGet();
1055 final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
1056 iq.setTo(jid);
1057 iq.query("http://jabber.org/protocol/disco#info");
1058 this.sendIqPacket(iq, new OnIqPacketReceived() {
1059
1060 @Override
1061 public void onIqPacketReceived(final Account account, final IqPacket packet) {
1062 if (packet.getType() == IqPacket.TYPE.RESULT) {
1063 boolean advancedStreamFeaturesLoaded;
1064 synchronized (XmppConnection.this.disco) {
1065 ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet);
1066 for (final ServiceDiscoveryResult.Identity id : result.getIdentities()) {
1067 if (mServerIdentity == Identity.UNKNOWN && id.getType().equals("im") &&
1068 id.getCategory().equals("server") && id.getName() != null &&
1069 jid.equals(account.getServer())) {
1070 switch (id.getName()) {
1071 case "Prosody":
1072 mServerIdentity = Identity.PROSODY;
1073 break;
1074 case "ejabberd":
1075 mServerIdentity = Identity.EJABBERD;
1076 break;
1077 case "Slack-XMPP":
1078 mServerIdentity = Identity.SLACK;
1079 break;
1080 }
1081 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": server name: " + id.getName());
1082 }
1083 }
1084 if (jid.equals(account.getServer())) {
1085 mXmppConnectionService.databaseBackend.insertDiscoveryResult(result);
1086 }
1087 disco.put(jid, result);
1088 advancedStreamFeaturesLoaded = disco.containsKey(account.getServer())
1089 && disco.containsKey(account.getJid().toBareJid());
1090 }
1091 if (advancedStreamFeaturesLoaded && (jid.equals(account.getServer()) || jid.equals(account.getJid().toBareJid()))) {
1092 enableAdvancedStreamFeatures();
1093 }
1094 } else {
1095 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not query disco info for " + jid.toString());
1096 }
1097 if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
1098 if (mPendingServiceDiscoveries.decrementAndGet() == 0
1099 && mWaitForDisco.compareAndSet(true, false)) {
1100 finalizeBind();
1101 }
1102 }
1103 }
1104 });
1105 }
1106
1107 private void finalizeBind() {
1108 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": online with resource " + account.getResource());
1109 if (bindListener != null) {
1110 bindListener.onBind(account);
1111 }
1112 changeStatus(Account.State.ONLINE);
1113 }
1114
1115 private void enableAdvancedStreamFeatures() {
1116 if (getFeatures().carbons() && !features.carbonsEnabled) {
1117 sendEnableCarbons();
1118 }
1119 if (getFeatures().blocking() && !features.blockListRequested) {
1120 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": Requesting block list");
1121 this.sendIqPacket(getIqGenerator().generateGetBlockList(), mXmppConnectionService.getIqParser());
1122 }
1123 for (final OnAdvancedStreamFeaturesLoaded listener : advancedStreamFeaturesLoadedListeners) {
1124 listener.onAdvancedStreamFeaturesAvailable(account);
1125 }
1126 }
1127
1128 private void sendServiceDiscoveryItems(final Jid server) {
1129 mPendingServiceDiscoveries.incrementAndGet();
1130 final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
1131 iq.setTo(server.toDomainJid());
1132 iq.query("http://jabber.org/protocol/disco#items");
1133 this.sendIqPacket(iq, new OnIqPacketReceived() {
1134
1135 @Override
1136 public void onIqPacketReceived(final Account account, final IqPacket packet) {
1137 if (packet.getType() == IqPacket.TYPE.RESULT) {
1138 final List<Element> elements = packet.query().getChildren();
1139 for (final Element element : elements) {
1140 if (element.getName().equals("item")) {
1141 final Jid jid = element.getAttributeAsJid("jid");
1142 if (jid != null && !jid.equals(account.getServer())) {
1143 sendServiceDiscoveryInfo(jid);
1144 }
1145 }
1146 }
1147 } else {
1148 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not query disco items of " + server);
1149 }
1150 if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
1151 if (mPendingServiceDiscoveries.decrementAndGet() == 0
1152 && mWaitForDisco.compareAndSet(true, false)) {
1153 finalizeBind();
1154 }
1155 }
1156 }
1157 });
1158 }
1159
1160 private void sendEnableCarbons() {
1161 final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
1162 iq.addChild("enable", "urn:xmpp:carbons:2");
1163 this.sendIqPacket(iq, new OnIqPacketReceived() {
1164
1165 @Override
1166 public void onIqPacketReceived(final Account account, final IqPacket packet) {
1167 if (!packet.hasChild("error")) {
1168 Log.d(Config.LOGTAG, account.getJid().toBareJid()
1169 + ": successfully enabled carbons");
1170 features.carbonsEnabled = true;
1171 } else {
1172 Log.d(Config.LOGTAG, account.getJid().toBareJid()
1173 + ": error enableing carbons " + packet.toString());
1174 }
1175 }
1176 });
1177 }
1178
1179 private void processStreamError(final Tag currentTag)
1180 throws XmlPullParserException, IOException {
1181 final Element streamError = tagReader.readElement(currentTag);
1182 if (streamError == null) {
1183 return;
1184 }
1185 if (streamError.hasChild("conflict")) {
1186 final String resource = account.getResource().split("\\.")[0];
1187 account.setResource(resource + "." + nextRandomId());
1188 Log.d(Config.LOGTAG,
1189 account.getJid().toBareJid() + ": switching resource due to conflict ("
1190 + account.getResource() + ")");
1191 throw new IOException();
1192 } else if (streamError.hasChild("host-unknown")) {
1193 throw new StreamErrorHostUnknown();
1194 } else if (streamError.hasChild("policy-violation")) {
1195 throw new StreamErrorPolicyViolation();
1196 } else {
1197 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": stream error "+streamError.toString());
1198 throw new StreamError();
1199 }
1200 }
1201
1202 private void sendStartStream() throws IOException {
1203 final Tag stream = Tag.start("stream:stream");
1204 stream.setAttribute("to", account.getServer().toString());
1205 stream.setAttribute("version", "1.0");
1206 stream.setAttribute("xml:lang", "en");
1207 stream.setAttribute("xmlns", "jabber:client");
1208 stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
1209 tagWriter.writeTag(stream);
1210 }
1211
1212 private String nextRandomId() {
1213 return new BigInteger(50, mXmppConnectionService.getRNG()).toString(32);
1214 }
1215
1216 public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) {
1217 packet.setFrom(account.getJid());
1218 return this.sendUnmodifiedIqPacket(packet, callback);
1219 }
1220
1221 private synchronized String sendUnmodifiedIqPacket(final IqPacket packet, final OnIqPacketReceived callback) {
1222 if (packet.getId() == null) {
1223 final String id = nextRandomId();
1224 packet.setAttribute("id", id);
1225 }
1226 if (callback != null) {
1227 synchronized (this.packetCallbacks) {
1228 packetCallbacks.put(packet.getId(), new Pair<>(packet, callback));
1229 }
1230 }
1231 this.sendPacket(packet);
1232 return packet.getId();
1233 }
1234
1235 public void sendMessagePacket(final MessagePacket packet) {
1236 this.sendPacket(packet);
1237 }
1238
1239 public void sendPresencePacket(final PresencePacket packet) {
1240 this.sendPacket(packet);
1241 }
1242
1243 private synchronized void sendPacket(final AbstractStanza packet) {
1244 if (stanzasSent == Integer.MAX_VALUE) {
1245 resetStreamId();
1246 disconnect(true);
1247 return;
1248 }
1249 synchronized (this.mStanzaQueue) {
1250 tagWriter.writeStanzaAsync(packet);
1251 if (packet instanceof AbstractAcknowledgeableStanza) {
1252 AbstractAcknowledgeableStanza stanza = (AbstractAcknowledgeableStanza) packet;
1253 ++stanzasSent;
1254 this.mStanzaQueue.append(stanzasSent, stanza);
1255 if (stanza instanceof MessagePacket && stanza.getId() != null && getFeatures().sm()) {
1256 if (Config.EXTENDED_SM_LOGGING) {
1257 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": requesting ack for message stanza #" + stanzasSent);
1258 }
1259 tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion));
1260 }
1261 }
1262 }
1263 }
1264
1265 public void sendPing() {
1266 if (!r()) {
1267 final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
1268 iq.setFrom(account.getJid());
1269 iq.addChild("ping", "urn:xmpp:ping");
1270 this.sendIqPacket(iq, null);
1271 }
1272 this.lastPingSent = SystemClock.elapsedRealtime();
1273 }
1274
1275 public void setOnMessagePacketReceivedListener(
1276 final OnMessagePacketReceived listener) {
1277 this.messageListener = listener;
1278 }
1279
1280 public void setOnUnregisteredIqPacketReceivedListener(
1281 final OnIqPacketReceived listener) {
1282 this.unregisteredIqListener = listener;
1283 }
1284
1285 public void setOnPresencePacketReceivedListener(
1286 final OnPresencePacketReceived listener) {
1287 this.presenceListener = listener;
1288 }
1289
1290 public void setOnJinglePacketReceivedListener(
1291 final OnJinglePacketReceived listener) {
1292 this.jingleListener = listener;
1293 }
1294
1295 public void setOnStatusChangedListener(final OnStatusChanged listener) {
1296 this.statusListener = listener;
1297 }
1298
1299 public void setOnBindListener(final OnBindListener listener) {
1300 this.bindListener = listener;
1301 }
1302
1303 public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) {
1304 this.acknowledgedListener = listener;
1305 }
1306
1307 public void addOnAdvancedStreamFeaturesAvailableListener(final OnAdvancedStreamFeaturesLoaded listener) {
1308 if (!this.advancedStreamFeaturesLoadedListeners.contains(listener)) {
1309 this.advancedStreamFeaturesLoadedListeners.add(listener);
1310 }
1311 }
1312
1313 public void waitForPush() {
1314 if (tagWriter.isActive()) {
1315 tagWriter.finish();
1316 new Thread(new Runnable() {
1317 @Override
1318 public void run() {
1319 try {
1320 while(!tagWriter.finished()) {
1321 Thread.sleep(10);
1322 }
1323 socket.close();
1324 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": closed tcp without closing stream");
1325 } catch (IOException | InterruptedException e) {
1326 return;
1327 }
1328 }
1329 }).start();
1330 } else {
1331 forceCloseSocket();
1332 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": closed tcp without closing stream (no waiting)");
1333 }
1334 }
1335
1336 private void forceCloseSocket() {
1337 if (socket != null) {
1338 try {
1339 socket.close();
1340 } catch (IOException e) {
1341 e.printStackTrace();
1342 }
1343 }
1344 }
1345
1346 public void interrupt() {
1347 Thread.currentThread().interrupt();
1348 }
1349
1350 public void disconnect(final boolean force) {
1351 interrupt();
1352 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": disconnecting force="+Boolean.valueOf(force));
1353 if (force) {
1354 tagWriter.forceClose();
1355 forceCloseSocket();
1356 } else {
1357 if (tagWriter.isActive()) {
1358 tagWriter.finish();
1359 try {
1360 int i = 0;
1361 boolean warned = false;
1362 while (!tagWriter.finished() && socket.isConnected() && i <= 10) {
1363 if (!warned) {
1364 Log.d(Config.LOGTAG, account.getJid().toBareJid()+": waiting for tag writer to finish");
1365 warned = true;
1366 }
1367 Thread.sleep(200);
1368 i++;
1369 }
1370 if (warned) {
1371 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": tag writer has finished");
1372 }
1373 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": closing stream");
1374 tagWriter.writeTag(Tag.end("stream:stream"));
1375 } catch (final IOException e) {
1376 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": io exception during disconnect ("+e.getMessage()+")");
1377 } catch (final InterruptedException e) {
1378 Log.d(Config.LOGTAG, "interrupted");
1379 }
1380 }
1381 }
1382 }
1383
1384 public void resetStreamId() {
1385 this.streamId = null;
1386 }
1387
1388 private List<Entry<Jid, ServiceDiscoveryResult>> findDiscoItemsByFeature(final String feature) {
1389 synchronized (this.disco) {
1390 final List<Entry<Jid, ServiceDiscoveryResult>> items = new ArrayList<>();
1391 for (final Entry<Jid, ServiceDiscoveryResult> cursor : this.disco.entrySet()) {
1392 if (cursor.getValue().getFeatures().contains(feature)) {
1393 items.add(cursor);
1394 }
1395 }
1396 return items;
1397 }
1398 }
1399
1400 public Jid findDiscoItemByFeature(final String feature) {
1401 final List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(feature);
1402 if (items.size() >= 1) {
1403 return items.get(0).getKey();
1404 }
1405 return null;
1406 }
1407
1408 public boolean r() {
1409 if (getFeatures().sm()) {
1410 this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
1411 return true;
1412 } else {
1413 return false;
1414 }
1415 }
1416
1417 public String getMucServer() {
1418 synchronized (this.disco) {
1419 for (final Entry<Jid, ServiceDiscoveryResult> cursor : disco.entrySet()) {
1420 final ServiceDiscoveryResult value = cursor.getValue();
1421 if (value.getFeatures().contains("http://jabber.org/protocol/muc")
1422 && !value.getFeatures().contains("jabber:iq:gateway")
1423 && !value.hasIdentity("conference", "irc")) {
1424 return cursor.getKey().toString();
1425 }
1426 }
1427 }
1428 return null;
1429 }
1430
1431 public int getTimeToNextAttempt() {
1432 final int interval = Math.min((int) (25 * Math.pow(1.3, attempt)), 300);
1433 final int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
1434 return interval - secondsSinceLast;
1435 }
1436
1437 public int getAttempt() {
1438 return this.attempt;
1439 }
1440
1441 public Features getFeatures() {
1442 return this.features;
1443 }
1444
1445 public long getLastSessionEstablished() {
1446 final long diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
1447 return System.currentTimeMillis() - diff;
1448 }
1449
1450 public long getLastConnect() {
1451 return this.lastConnect;
1452 }
1453
1454 public long getLastPingSent() {
1455 return this.lastPingSent;
1456 }
1457
1458 public long getLastDiscoStarted() {
1459 return this.lastDiscoStarted;
1460 }
1461 public long getLastPacketReceived() {
1462 return this.lastPacketReceived;
1463 }
1464
1465 public void sendActive() {
1466 this.sendPacket(new ActivePacket());
1467 }
1468
1469 public void sendInactive() {
1470 this.sendPacket(new InactivePacket());
1471 }
1472
1473 public void resetAttemptCount() {
1474 this.attempt = 0;
1475 this.lastConnect = 0;
1476 }
1477
1478 public void setInteractive(boolean interactive) {
1479 this.mInteractive = interactive;
1480 }
1481
1482 public Identity getServerIdentity() {
1483 return mServerIdentity;
1484 }
1485
1486 private class UnauthorizedException extends IOException {
1487
1488 }
1489
1490 private class SecurityException extends IOException {
1491
1492 }
1493
1494 private class IncompatibleServerException extends IOException {
1495
1496 }
1497
1498 private class StreamErrorHostUnknown extends StreamError {
1499
1500 }
1501
1502 private class StreamErrorPolicyViolation extends StreamError {
1503
1504 }
1505
1506 private class StreamError extends IOException {
1507
1508 }
1509
1510 public enum Identity {
1511 FACEBOOK,
1512 SLACK,
1513 EJABBERD,
1514 PROSODY,
1515 NIMBUZZ,
1516 UNKNOWN
1517 }
1518
1519 public class Features {
1520 XmppConnection connection;
1521 private boolean carbonsEnabled = false;
1522 private boolean encryptionEnabled = false;
1523 private boolean blockListRequested = false;
1524
1525 public Features(final XmppConnection connection) {
1526 this.connection = connection;
1527 }
1528
1529 private boolean hasDiscoFeature(final Jid server, final String feature) {
1530 synchronized (XmppConnection.this.disco) {
1531 return connection.disco.containsKey(server) &&
1532 connection.disco.get(server).getFeatures().contains(feature);
1533 }
1534 }
1535
1536 public boolean carbons() {
1537 return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2");
1538 }
1539
1540 public boolean blocking() {
1541 return hasDiscoFeature(account.getServer(), Xmlns.BLOCKING);
1542 }
1543
1544 public boolean register() {
1545 return hasDiscoFeature(account.getServer(), Xmlns.REGISTER);
1546 }
1547
1548 public boolean sm() {
1549 return streamId != null
1550 || (connection.streamFeatures != null && connection.streamFeatures.hasChild("sm"));
1551 }
1552
1553 public boolean csi() {
1554 return connection.streamFeatures != null && connection.streamFeatures.hasChild("csi", "urn:xmpp:csi:0");
1555 }
1556
1557 public boolean pep() {
1558 synchronized (XmppConnection.this.disco) {
1559 ServiceDiscoveryResult info = disco.get(account.getJid().toBareJid());
1560 return info != null && info.hasIdentity("pubsub", "pep");
1561 }
1562 }
1563
1564 public boolean pepPersistent() {
1565 synchronized (XmppConnection.this.disco) {
1566 ServiceDiscoveryResult info = disco.get(account.getJid().toBareJid());
1567 return info != null && info.getFeatures().contains("http://jabber.org/protocol/pubsub#persistent-items");
1568 }
1569 }
1570
1571 public boolean mam() {
1572 return hasDiscoFeature(account.getJid().toBareJid(), "urn:xmpp:mam:0")
1573 || hasDiscoFeature(account.getServer(), "urn:xmpp:mam:0");
1574 }
1575
1576 public boolean push() {
1577 return hasDiscoFeature(account.getJid().toBareJid(), "urn:xmpp:push:0")
1578 || hasDiscoFeature(account.getServer(), "urn:xmpp:push:0");
1579 }
1580
1581 public boolean rosterVersioning() {
1582 return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver");
1583 }
1584
1585 public void setBlockListRequested(boolean value) {
1586 this.blockListRequested = value;
1587 }
1588
1589 public boolean httpUpload(long filesize) {
1590 if (Config.DISABLE_HTTP_UPLOAD) {
1591 return false;
1592 } else {
1593 List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(Xmlns.HTTP_UPLOAD);
1594 if (items.size() > 0) {
1595 try {
1596 long maxsize = Long.parseLong(items.get(0).getValue().getExtendedDiscoInformation(Xmlns.HTTP_UPLOAD, "max-file-size"));
1597 if(filesize <= maxsize) {
1598 return true;
1599 } else {
1600 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": http upload is not available for files with size "+filesize+" (max is "+maxsize+")");
1601 return false;
1602 }
1603 } catch (Exception e) {
1604 return true;
1605 }
1606 } else {
1607 return false;
1608 }
1609 }
1610 }
1611
1612 public long getMaxHttpUploadSize() {
1613 List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(Xmlns.HTTP_UPLOAD);
1614 if (items.size() > 0) {
1615 try {
1616 return Long.parseLong(items.get(0).getValue().getExtendedDiscoInformation(Xmlns.HTTP_UPLOAD, "max-file-size"));
1617 } catch (Exception e) {
1618 return -1;
1619 }
1620 } else {
1621 return -1;
1622 }
1623 }
1624 }
1625
1626 private IqGenerator getIqGenerator() {
1627 return mXmppConnectionService.getIqGenerator();
1628 }
1629}