1package eu.siacs.conversations.xmpp;
2
3import java.io.IOException;
4import java.io.InputStream;
5import java.io.OutputStream;
6import java.math.BigInteger;
7import java.net.Socket;
8import java.net.UnknownHostException;
9import java.security.KeyManagementException;
10import java.security.NoSuchAlgorithmException;
11import java.security.SecureRandom;
12import java.util.ArrayList;
13import java.util.Arrays;
14import java.util.HashMap;
15import java.util.Hashtable;
16import java.util.LinkedList;
17import java.util.List;
18import java.util.Map.Entry;
19
20import javax.net.ssl.HostnameVerifier;
21import javax.net.ssl.SSLContext;
22import javax.net.ssl.SSLSocket;
23import javax.net.ssl.SSLSocketFactory;
24
25import javax.net.ssl.X509TrustManager;
26
27import org.xmlpull.v1.XmlPullParserException;
28
29import de.duenndns.ssl.MemorizingTrustManager;
30
31import android.content.Context;
32import android.content.SharedPreferences;
33import android.os.Bundle;
34import android.os.PowerManager;
35import android.os.PowerManager.WakeLock;
36import android.os.SystemClock;
37import android.preference.PreferenceManager;
38import android.util.Log;
39import android.util.SparseArray;
40import eu.siacs.conversations.Config;
41import eu.siacs.conversations.entities.Account;
42import eu.siacs.conversations.services.XmppConnectionService;
43import eu.siacs.conversations.utils.CryptoHelper;
44import eu.siacs.conversations.utils.DNSHelper;
45import eu.siacs.conversations.utils.zlib.ZLibOutputStream;
46import eu.siacs.conversations.utils.zlib.ZLibInputStream;
47import eu.siacs.conversations.xml.Element;
48import eu.siacs.conversations.xml.Tag;
49import eu.siacs.conversations.xml.TagWriter;
50import eu.siacs.conversations.xml.XmlReader;
51import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
52import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
53import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
54import eu.siacs.conversations.xmpp.stanzas.IqPacket;
55import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
56import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
57import eu.siacs.conversations.xmpp.stanzas.csi.ActivePacket;
58import eu.siacs.conversations.xmpp.stanzas.csi.InactivePacket;
59import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
60import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
61import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
62import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
63
64public class XmppConnection implements Runnable {
65
66 protected Account account;
67
68 private WakeLock wakeLock;
69
70 private SecureRandom mRandom;
71
72 private Socket socket;
73 private XmlReader tagReader;
74 private TagWriter tagWriter;
75
76 private Features features = new Features(this);
77
78 private boolean shouldBind = true;
79 private boolean shouldAuthenticate = true;
80 private Element streamFeatures;
81 private HashMap<String, List<String>> disco = new HashMap<String, List<String>>();
82
83 private String streamId = null;
84 private int smVersion = 3;
85 private SparseArray<String> messageReceipts = new SparseArray<String>();
86
87 private boolean usingCompression = false;
88 private boolean usingEncryption = false;
89
90 private int stanzasReceived = 0;
91 private int stanzasSent = 0;
92
93 private long lastPaketReceived = 0;
94 private long lastPingSent = 0;
95 private long lastConnect = 0;
96 private long lastSessionStarted = 0;
97
98 private int attempt = 0;
99
100 private static final int PACKET_IQ = 0;
101 private static final int PACKET_MESSAGE = 1;
102 private static final int PACKET_PRESENCE = 2;
103
104 private Hashtable<String, PacketReceived> packetCallbacks = new Hashtable<String, PacketReceived>();
105 private OnPresencePacketReceived presenceListener = null;
106 private OnJinglePacketReceived jingleListener = null;
107 private OnIqPacketReceived unregisteredIqListener = null;
108 private OnMessagePacketReceived messageListener = null;
109 private OnStatusChanged statusListener = null;
110 private OnBindListener bindListener = null;
111 private OnMessageAcknowledged acknowledgedListener = null;
112 private MemorizingTrustManager mMemorizingTrustManager;
113 private final Context applicationContext;
114
115 public XmppConnection(Account account, XmppConnectionService service) {
116 this.mRandom = service.getRNG();
117 this.mMemorizingTrustManager = service.getMemorizingTrustManager();
118 this.account = account;
119 this.wakeLock = service.getPowerManager().newWakeLock(
120 PowerManager.PARTIAL_WAKE_LOCK, account.getJid());
121 tagWriter = new TagWriter();
122 applicationContext = service.getApplicationContext();
123 }
124
125 protected void changeStatus(int nextStatus) {
126 if (account.getStatus() != nextStatus) {
127 if ((nextStatus == Account.STATUS_OFFLINE)
128 && (account.getStatus() != Account.STATUS_CONNECTING)
129 && (account.getStatus() != Account.STATUS_ONLINE)
130 && (account.getStatus() != Account.STATUS_DISABLED)) {
131 return;
132 }
133 if (nextStatus == Account.STATUS_ONLINE) {
134 this.attempt = 0;
135 }
136 account.setStatus(nextStatus);
137 if (statusListener != null) {
138 statusListener.onStatusChanged(account);
139 }
140 }
141 }
142
143 protected void connect() {
144 Log.d(Config.LOGTAG, account.getJid() + ": connecting");
145 usingCompression = false;
146 usingEncryption = false;
147 lastConnect = SystemClock.elapsedRealtime();
148 lastPingSent = SystemClock.elapsedRealtime();
149 this.attempt++;
150 try {
151 shouldAuthenticate = shouldBind = !account
152 .isOptionSet(Account.OPTION_REGISTER);
153 tagReader = new XmlReader(wakeLock);
154 tagWriter = new TagWriter();
155 packetCallbacks.clear();
156 this.changeStatus(Account.STATUS_CONNECTING);
157 Bundle namePort = DNSHelper.getSRVRecord(account.getServer());
158 if ("timeout".equals(namePort.getString("error"))) {
159 Log.d(Config.LOGTAG, account.getJid() + ": dns timeout");
160 this.changeStatus(Account.STATUS_OFFLINE);
161 return;
162 }
163 String srvRecordServer = namePort.getString("name");
164 String srvIpServer = namePort.getString("ipv4");
165 int srvRecordPort = namePort.getInt("port");
166 if (srvRecordServer != null) {
167 if (srvIpServer != null) {
168 Log.d(Config.LOGTAG, account.getJid()
169 + ": using values from dns " + srvRecordServer
170 + "[" + srvIpServer + "]:" + srvRecordPort);
171 socket = new Socket(srvIpServer, srvRecordPort);
172 } else {
173 boolean socketError = true;
174 int srvIndex = 0;
175 while (socketError && namePort.containsKey("name" + srvIndex)){
176 try {
177 srvRecordServer = namePort.getString("name" + srvIndex);
178 srvRecordPort = namePort.getInt("port" + srvIndex);
179 Log.d(Config.LOGTAG, account.getJid()
180 + ": using values from dns " + srvRecordServer
181 + ":" + srvRecordPort);
182 socket = new Socket(srvRecordServer, srvRecordPort);
183 socketError = false;
184 } catch (UnknownHostException e) {
185 srvIndex++;
186 } catch (IOException e) {
187 srvIndex++;
188 }
189 }
190 }
191 } else if (namePort.containsKey("error")
192 && "nosrv".equals(namePort.getString("error", null))) {
193 socket = new Socket(account.getServer(), 5222);
194 } else {
195 Log.d(Config.LOGTAG, account.getJid()
196 + ": timeout in DNS resolution");
197 changeStatus(Account.STATUS_OFFLINE);
198 return;
199 }
200 OutputStream out = socket.getOutputStream();
201 tagWriter.setOutputStream(out);
202 InputStream in = socket.getInputStream();
203 tagReader.setInputStream(in);
204 tagWriter.beginDocument();
205 sendStartStream();
206 Tag nextTag;
207 while ((nextTag = tagReader.readTag()) != null) {
208 if (nextTag.isStart("stream")) {
209 processStream(nextTag);
210 break;
211 } else {
212 Log.d(Config.LOGTAG,
213 "found unexpected tag: " + nextTag.getName());
214 return;
215 }
216 }
217 if (socket.isConnected()) {
218 socket.close();
219 }
220 } catch (UnknownHostException e) {
221 this.changeStatus(Account.STATUS_SERVER_NOT_FOUND);
222 if (wakeLock.isHeld()) {
223 try {
224 wakeLock.release();
225 } catch (RuntimeException re) {
226 }
227 }
228 return;
229 } catch (IOException e) {
230 this.changeStatus(Account.STATUS_OFFLINE);
231 if (wakeLock.isHeld()) {
232 try {
233 wakeLock.release();
234 } catch (RuntimeException re) {
235 }
236 }
237 return;
238 } catch (NoSuchAlgorithmException e) {
239 this.changeStatus(Account.STATUS_OFFLINE);
240 Log.d(Config.LOGTAG, "compression exception " + e.getMessage());
241 if (wakeLock.isHeld()) {
242 try {
243 wakeLock.release();
244 } catch (RuntimeException re) {
245 }
246 }
247 return;
248 } catch (XmlPullParserException e) {
249 this.changeStatus(Account.STATUS_OFFLINE);
250 Log.d(Config.LOGTAG, "xml exception " + e.getMessage());
251 if (wakeLock.isHeld()) {
252 try {
253 wakeLock.release();
254 } catch (RuntimeException re) {
255 }
256 }
257 return;
258 }
259
260 }
261
262 @Override
263 public void run() {
264 connect();
265 }
266
267 private void processStream(Tag currentTag) throws XmlPullParserException,
268 IOException, NoSuchAlgorithmException {
269 Tag nextTag = tagReader.readTag();
270 while ((nextTag != null) && (!nextTag.isEnd("stream"))) {
271 if (nextTag.isStart("error")) {
272 processStreamError(nextTag);
273 } else if (nextTag.isStart("features")) {
274 processStreamFeatures(nextTag);
275 } else if (nextTag.isStart("proceed")) {
276 switchOverToTls(nextTag);
277 } else if (nextTag.isStart("compressed")) {
278 switchOverToZLib(nextTag);
279 } else if (nextTag.isStart("success")) {
280 Log.d(Config.LOGTAG, account.getJid() + ": logged in");
281 tagReader.readTag();
282 tagReader.reset();
283 sendStartStream();
284 processStream(tagReader.readTag());
285 break;
286 } else if (nextTag.isStart("failure")) {
287 tagReader.readElement(nextTag);
288 changeStatus(Account.STATUS_UNAUTHORIZED);
289 } else if (nextTag.isStart("challenge")) {
290 String challange = tagReader.readElement(nextTag).getContent();
291 Element response = new Element("response");
292 response.setAttribute("xmlns",
293 "urn:ietf:params:xml:ns:xmpp-sasl");
294 response.setContent(CryptoHelper.saslDigestMd5(account,
295 challange, mRandom));
296 tagWriter.writeElement(response);
297 } else if (nextTag.isStart("enabled")) {
298 Element enabled = tagReader.readElement(nextTag);
299 if ("true".equals(enabled.getAttribute("resume"))) {
300 this.streamId = enabled.getAttribute("id");
301 Log.d(Config.LOGTAG, account.getJid()
302 + ": stream managment(" + smVersion
303 + ") enabled (resumable)");
304 } else {
305 Log.d(Config.LOGTAG, account.getJid()
306 + ": stream managment(" + smVersion + ") enabled");
307 }
308 this.lastSessionStarted = SystemClock.elapsedRealtime();
309 this.stanzasReceived = 0;
310 RequestPacket r = new RequestPacket(smVersion);
311 tagWriter.writeStanzaAsync(r);
312 } else if (nextTag.isStart("resumed")) {
313 lastPaketReceived = SystemClock.elapsedRealtime();
314 Element resumed = tagReader.readElement(nextTag);
315 String h = resumed.getAttribute("h");
316 try {
317 int serverCount = Integer.parseInt(h);
318 if (serverCount != stanzasSent) {
319 Log.d(Config.LOGTAG, account.getJid()
320 + ": session resumed with lost packages");
321 stanzasSent = serverCount;
322 } else {
323 Log.d(Config.LOGTAG, account.getJid()
324 + ": session resumed");
325 }
326 if (acknowledgedListener != null) {
327 for (int i = 0; i < messageReceipts.size(); ++i) {
328 if (serverCount >= messageReceipts.keyAt(i)) {
329 acknowledgedListener.onMessageAcknowledged(
330 account, messageReceipts.valueAt(i));
331 }
332 }
333 }
334 messageReceipts.clear();
335 } catch (NumberFormatException e) {
336
337 }
338 sendInitialPing();
339
340 } else if (nextTag.isStart("r")) {
341 tagReader.readElement(nextTag);
342 AckPacket ack = new AckPacket(this.stanzasReceived, smVersion);
343 tagWriter.writeStanzaAsync(ack);
344 } else if (nextTag.isStart("a")) {
345 Element ack = tagReader.readElement(nextTag);
346 lastPaketReceived = SystemClock.elapsedRealtime();
347 int serverSequence = Integer.parseInt(ack.getAttribute("h"));
348 String msgId = this.messageReceipts.get(serverSequence);
349 if (msgId != null) {
350 if (this.acknowledgedListener != null) {
351 this.acknowledgedListener.onMessageAcknowledged(
352 account, msgId);
353 }
354 this.messageReceipts.remove(serverSequence);
355 }
356 } else if (nextTag.isStart("failed")) {
357 tagReader.readElement(nextTag);
358 Log.d(Config.LOGTAG, account.getJid() + ": resumption failed");
359 streamId = null;
360 if (account.getStatus() != Account.STATUS_ONLINE) {
361 sendBindRequest();
362 }
363 } else if (nextTag.isStart("iq")) {
364 processIq(nextTag);
365 } else if (nextTag.isStart("message")) {
366 processMessage(nextTag);
367 } else if (nextTag.isStart("presence")) {
368 processPresence(nextTag);
369 }
370 nextTag = tagReader.readTag();
371 }
372 if (account.getStatus() == Account.STATUS_ONLINE) {
373 account.setStatus(Account.STATUS_OFFLINE);
374 if (statusListener != null) {
375 statusListener.onStatusChanged(account);
376 }
377 }
378 }
379
380 private void sendInitialPing() {
381 Log.d(Config.LOGTAG, account.getJid() + ": sending intial ping");
382 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
383 iq.setFrom(account.getFullJid());
384 iq.addChild("ping", "urn:xmpp:ping");
385 this.sendIqPacket(iq, new OnIqPacketReceived() {
386
387 @Override
388 public void onIqPacketReceived(Account account, IqPacket packet) {
389 Log.d(Config.LOGTAG, account.getJid()
390 + ": online with resource " + account.getResource());
391 changeStatus(Account.STATUS_ONLINE);
392 }
393 });
394 }
395
396 private Element processPacket(Tag currentTag, int packetType)
397 throws XmlPullParserException, IOException {
398 Element element;
399 switch (packetType) {
400 case PACKET_IQ:
401 element = new IqPacket();
402 break;
403 case PACKET_MESSAGE:
404 element = new MessagePacket();
405 break;
406 case PACKET_PRESENCE:
407 element = new PresencePacket();
408 break;
409 default:
410 return null;
411 }
412 element.setAttributes(currentTag.getAttributes());
413 Tag nextTag = tagReader.readTag();
414 if (nextTag == null) {
415 throw new IOException("interrupted mid tag");
416 }
417 while (!nextTag.isEnd(element.getName())) {
418 if (!nextTag.isNo()) {
419 Element child = tagReader.readElement(nextTag);
420 String type = currentTag.getAttribute("type");
421 if (packetType == PACKET_IQ
422 && "jingle".equals(child.getName())
423 && ("set".equalsIgnoreCase(type) || "get"
424 .equalsIgnoreCase(type))) {
425 element = new JinglePacket();
426 element.setAttributes(currentTag.getAttributes());
427 }
428 element.addChild(child);
429 }
430 nextTag = tagReader.readTag();
431 if (nextTag == null) {
432 throw new IOException("interrupted mid tag");
433 }
434 }
435 ++stanzasReceived;
436 lastPaketReceived = SystemClock.elapsedRealtime();
437 return element;
438 }
439
440 private void processIq(Tag currentTag) throws XmlPullParserException,
441 IOException {
442 IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
443
444 if (packet.getId() == null) {
445 return; // an iq packet without id is definitely invalid
446 }
447
448 if (packet instanceof JinglePacket) {
449 if (this.jingleListener != null) {
450 this.jingleListener.onJinglePacketReceived(account,
451 (JinglePacket) packet);
452 }
453 } else {
454 if (packetCallbacks.containsKey(packet.getId())) {
455 if (packetCallbacks.get(packet.getId()) instanceof OnIqPacketReceived) {
456 ((OnIqPacketReceived) packetCallbacks.get(packet.getId()))
457 .onIqPacketReceived(account, packet);
458 }
459
460 packetCallbacks.remove(packet.getId());
461 } else if ((packet.getType() == IqPacket.TYPE_GET || packet
462 .getType() == IqPacket.TYPE_SET)
463 && this.unregisteredIqListener != null) {
464 this.unregisteredIqListener.onIqPacketReceived(account, packet);
465 }
466 }
467 }
468
469 private void processMessage(Tag currentTag) throws XmlPullParserException,
470 IOException {
471 MessagePacket packet = (MessagePacket) processPacket(currentTag,
472 PACKET_MESSAGE);
473 String id = packet.getAttribute("id");
474 if ((id != null) && (packetCallbacks.containsKey(id))) {
475 if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) {
476 ((OnMessagePacketReceived) packetCallbacks.get(id))
477 .onMessagePacketReceived(account, packet);
478 }
479 packetCallbacks.remove(id);
480 } else if (this.messageListener != null) {
481 this.messageListener.onMessagePacketReceived(account, packet);
482 }
483 }
484
485 private void processPresence(Tag currentTag) throws XmlPullParserException,
486 IOException {
487 PresencePacket packet = (PresencePacket) processPacket(currentTag,
488 PACKET_PRESENCE);
489 String id = packet.getAttribute("id");
490 if ((id != null) && (packetCallbacks.containsKey(id))) {
491 if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) {
492 ((OnPresencePacketReceived) packetCallbacks.get(id))
493 .onPresencePacketReceived(account, packet);
494 }
495 packetCallbacks.remove(id);
496 } else if (this.presenceListener != null) {
497 this.presenceListener.onPresencePacketReceived(account, packet);
498 }
499 }
500
501 private void sendCompressionZlib() throws IOException {
502 Element compress = new Element("compress");
503 compress.setAttribute("xmlns", "http://jabber.org/protocol/compress");
504 compress.addChild("method").setContent("zlib");
505 tagWriter.writeElement(compress);
506 }
507
508 private void switchOverToZLib(Tag currentTag)
509 throws XmlPullParserException, IOException,
510 NoSuchAlgorithmException {
511 tagReader.readTag(); // read tag close
512 tagWriter.setOutputStream(new ZLibOutputStream(tagWriter
513 .getOutputStream()));
514 tagReader
515 .setInputStream(new ZLibInputStream(tagReader.getInputStream()));
516
517 sendStartStream();
518 Log.d(Config.LOGTAG, account.getJid() + ": compression enabled");
519 usingCompression = true;
520 processStream(tagReader.readTag());
521 }
522
523 private void sendStartTLS() throws IOException {
524 Tag startTLS = Tag.empty("starttls");
525 startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
526 tagWriter.writeTag(startTLS);
527 }
528
529 private SharedPreferences getPreferences() {
530 return PreferenceManager.getDefaultSharedPreferences(applicationContext);
531 }
532
533 private boolean enableLegacySSL() {
534 return getPreferences().getBoolean("enable_legacy_ssl", false);
535 }
536
537 private void switchOverToTls(Tag currentTag) throws XmlPullParserException,
538 IOException {
539 tagReader.readTag();
540 try {
541 SSLContext sc = SSLContext.getInstance("TLS");
542 sc.init(null,
543 new X509TrustManager[] { this.mMemorizingTrustManager },
544 mRandom);
545 SSLSocketFactory factory = sc.getSocketFactory();
546
547 HostnameVerifier verifier = this.mMemorizingTrustManager
548 .wrapHostnameVerifier(new org.apache.http.conn.ssl.StrictHostnameVerifier());
549 SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,
550 socket.getInetAddress().getHostAddress(), socket.getPort(),
551 true);
552
553 // Support all protocols except legacy SSL.
554 // The min SDK version prevents us having to worry about SSLv2. In future, this may be
555 // true of SSLv3 as well.
556 final String[] supportProtocols;
557 if (enableLegacySSL()) {
558 supportProtocols = sslSocket.getSupportedProtocols();
559 } else {
560 final List<String> supportedProtocols = new LinkedList<String>(Arrays.asList(
561 sslSocket.getSupportedProtocols()));
562 supportedProtocols.remove("SSLv3");
563 supportProtocols = new String[supportedProtocols.size()];
564 supportedProtocols.toArray(supportProtocols);
565 }
566 sslSocket.setEnabledProtocols(supportProtocols);
567
568 if (verifier != null
569 && !verifier.verify(account.getServer(),
570 sslSocket.getSession())) {
571 Log.d(Config.LOGTAG, account.getJid()
572 + ": host mismatch in TLS connection");
573 sslSocket.close();
574 throw new IOException();
575 }
576 tagReader.setInputStream(sslSocket.getInputStream());
577 tagWriter.setOutputStream(sslSocket.getOutputStream());
578 sendStartStream();
579 Log.d(Config.LOGTAG, account.getJid()
580 + ": TLS connection established");
581 usingEncryption = true;
582 processStream(tagReader.readTag());
583 sslSocket.close();
584 } catch (NoSuchAlgorithmException e1) {
585 e1.printStackTrace();
586 } catch (KeyManagementException e) {
587 e.printStackTrace();
588 }
589 }
590
591 private void sendSaslAuthPlain() throws IOException {
592 String saslString = CryptoHelper.saslPlain(account.getUsername(),
593 account.getPassword());
594 Element auth = new Element("auth");
595 auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
596 auth.setAttribute("mechanism", "PLAIN");
597 auth.setContent(saslString);
598 tagWriter.writeElement(auth);
599 }
600
601 private void sendSaslAuthDigestMd5() throws IOException {
602 Element auth = new Element("auth");
603 auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
604 auth.setAttribute("mechanism", "DIGEST-MD5");
605 tagWriter.writeElement(auth);
606 }
607
608 private void processStreamFeatures(Tag currentTag)
609 throws XmlPullParserException, IOException {
610 this.streamFeatures = tagReader.readElement(currentTag);
611 if (this.streamFeatures.hasChild("starttls") && !usingEncryption) {
612 sendStartTLS();
613 } else if (compressionAvailable()) {
614 sendCompressionZlib();
615 } else if (this.streamFeatures.hasChild("register")
616 && account.isOptionSet(Account.OPTION_REGISTER) && usingEncryption) {
617 sendRegistryRequest();
618 } else if (!this.streamFeatures.hasChild("register")
619 && account.isOptionSet(Account.OPTION_REGISTER)) {
620 changeStatus(Account.STATUS_REGISTRATION_NOT_SUPPORTED);
621 disconnect(true);
622 } else if (this.streamFeatures.hasChild("mechanisms")
623 && shouldAuthenticate && usingEncryption) {
624 List<String> mechanisms = extractMechanisms(streamFeatures
625 .findChild("mechanisms"));
626 if (mechanisms.contains("PLAIN")) {
627 sendSaslAuthPlain();
628 } else if (mechanisms.contains("DIGEST-MD5")) {
629 sendSaslAuthDigestMd5();
630 }
631 } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:"
632 + smVersion)
633 && streamId != null) {
634 ResumePacket resume = new ResumePacket(this.streamId,
635 stanzasReceived, smVersion);
636 this.tagWriter.writeStanzaAsync(resume);
637 } else if (this.streamFeatures.hasChild("bind") && shouldBind) {
638 sendBindRequest();
639 } else {
640 Log.d(Config.LOGTAG,account.getJid()+": incompatible server. disconnecting");
641 disconnect(true);
642 }
643 }
644
645 private boolean compressionAvailable() {
646 if (!this.streamFeatures.hasChild("compression",
647 "http://jabber.org/features/compress"))
648 return false;
649 if (!ZLibOutputStream.SUPPORTED)
650 return false;
651 if (!account.isOptionSet(Account.OPTION_USECOMPRESSION))
652 return false;
653
654 Element compression = this.streamFeatures.findChild("compression",
655 "http://jabber.org/features/compress");
656 for (Element child : compression.getChildren()) {
657 if (!"method".equals(child.getName()))
658 continue;
659
660 if ("zlib".equalsIgnoreCase(child.getContent())) {
661 return true;
662 }
663 }
664 return false;
665 }
666
667 private List<String> extractMechanisms(Element stream) {
668 ArrayList<String> mechanisms = new ArrayList<String>(stream
669 .getChildren().size());
670 for (Element child : stream.getChildren()) {
671 mechanisms.add(child.getContent());
672 }
673 return mechanisms;
674 }
675
676 private void sendRegistryRequest() {
677 IqPacket register = new IqPacket(IqPacket.TYPE_GET);
678 register.query("jabber:iq:register");
679 register.setTo(account.getServer());
680 sendIqPacket(register, new OnIqPacketReceived() {
681
682 @Override
683 public void onIqPacketReceived(Account account, IqPacket packet) {
684 Element instructions = packet.query().findChild("instructions");
685 if (packet.query().hasChild("username")
686 && (packet.query().hasChild("password"))) {
687 IqPacket register = new IqPacket(IqPacket.TYPE_SET);
688 Element username = new Element("username")
689 .setContent(account.getUsername());
690 Element password = new Element("password")
691 .setContent(account.getPassword());
692 register.query("jabber:iq:register").addChild(username);
693 register.query().addChild(password);
694 sendIqPacket(register, new OnIqPacketReceived() {
695
696 @Override
697 public void onIqPacketReceived(Account account,
698 IqPacket packet) {
699 if (packet.getType() == IqPacket.TYPE_RESULT) {
700 account.setOption(Account.OPTION_REGISTER,
701 false);
702 changeStatus(Account.STATUS_REGISTRATION_SUCCESSFULL);
703 } else if (packet.hasChild("error")
704 && (packet.findChild("error")
705 .hasChild("conflict"))) {
706 changeStatus(Account.STATUS_REGISTRATION_CONFLICT);
707 } else {
708 changeStatus(Account.STATUS_REGISTRATION_FAILED);
709 Log.d(Config.LOGTAG, packet.toString());
710 }
711 disconnect(true);
712 }
713 });
714 } else {
715 changeStatus(Account.STATUS_REGISTRATION_FAILED);
716 disconnect(true);
717 Log.d(Config.LOGTAG, account.getJid()
718 + ": could not register. instructions are"
719 + instructions.getContent());
720 }
721 }
722 });
723 }
724
725 private void sendBindRequest() throws IOException {
726 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
727 iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind")
728 .addChild("resource").setContent(account.getResource());
729 this.sendUnboundIqPacket(iq, new OnIqPacketReceived() {
730 @Override
731 public void onIqPacketReceived(Account account, IqPacket packet) {
732 Element bind = packet.findChild("bind");
733 if (bind != null) {
734 Element jid = bind.findChild("jid");
735 if (jid != null && jid.getContent() != null) {
736 account.setResource(jid.getContent().split("/", 2)[1]);
737 if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) {
738 smVersion = 3;
739 EnablePacket enable = new EnablePacket(smVersion);
740 tagWriter.writeStanzaAsync(enable);
741 stanzasSent = 0;
742 messageReceipts.clear();
743 } else if (streamFeatures.hasChild("sm",
744 "urn:xmpp:sm:2")) {
745 smVersion = 2;
746 EnablePacket enable = new EnablePacket(smVersion);
747 tagWriter.writeStanzaAsync(enable);
748 stanzasSent = 0;
749 messageReceipts.clear();
750 }
751 sendServiceDiscoveryInfo(account.getServer());
752 sendServiceDiscoveryItems(account.getServer());
753 if (bindListener != null) {
754 bindListener.onBind(account);
755 }
756 sendInitialPing();
757 } else {
758 disconnect(true);
759 }
760 } else {
761 disconnect(true);
762 }
763 }
764 });
765 if (this.streamFeatures.hasChild("session")) {
766 Log.d(Config.LOGTAG, account.getJid()
767 + ": sending deprecated session");
768 IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
769 startSession.addChild("session",
770 "urn:ietf:params:xml:ns:xmpp-session");
771 this.sendUnboundIqPacket(startSession, null);
772 }
773 }
774
775 private void sendServiceDiscoveryInfo(final String server) {
776 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
777 iq.setTo(server);
778 iq.query("http://jabber.org/protocol/disco#info");
779 this.sendIqPacket(iq, new OnIqPacketReceived() {
780
781 @Override
782 public void onIqPacketReceived(Account account, IqPacket packet) {
783 List<Element> elements = packet.query().getChildren();
784 List<String> features = new ArrayList<String>();
785 for (int i = 0; i < elements.size(); ++i) {
786 if (elements.get(i).getName().equals("feature")) {
787 features.add(elements.get(i).getAttribute("var"));
788 }
789 }
790 disco.put(server, features);
791
792 if (account.getServer().equals(server)) {
793 enableAdvancedStreamFeatures();
794 }
795 }
796 });
797 }
798
799 private void enableAdvancedStreamFeatures() {
800 if (getFeatures().carbons()) {
801 sendEnableCarbons();
802 }
803 }
804
805 private void sendServiceDiscoveryItems(final String server) {
806 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
807 iq.setTo(server);
808 iq.query("http://jabber.org/protocol/disco#items");
809 this.sendIqPacket(iq, new OnIqPacketReceived() {
810
811 @Override
812 public void onIqPacketReceived(Account account, IqPacket packet) {
813 List<Element> elements = packet.query().getChildren();
814 for (int i = 0; i < elements.size(); ++i) {
815 if (elements.get(i).getName().equals("item")) {
816 String jid = elements.get(i).getAttribute("jid");
817 sendServiceDiscoveryInfo(jid);
818 }
819 }
820 }
821 });
822 }
823
824 private void sendEnableCarbons() {
825 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
826 iq.addChild("enable", "urn:xmpp:carbons:2");
827 this.sendIqPacket(iq, new OnIqPacketReceived() {
828
829 @Override
830 public void onIqPacketReceived(Account account, IqPacket packet) {
831 if (!packet.hasChild("error")) {
832 Log.d(Config.LOGTAG, account.getJid()
833 + ": successfully enabled carbons");
834 } else {
835 Log.d(Config.LOGTAG, account.getJid()
836 + ": error enableing carbons " + packet.toString());
837 }
838 }
839 });
840 }
841
842 private void processStreamError(Tag currentTag)
843 throws XmlPullParserException, IOException {
844 Element streamError = tagReader.readElement(currentTag);
845 if (streamError != null && streamError.hasChild("conflict")) {
846 String resource = account.getResource().split("\\.")[0];
847 account.setResource(resource + "." + nextRandomId());
848 Log.d(Config.LOGTAG,
849 account.getJid() + ": switching resource due to conflict ("
850 + account.getResource() + ")");
851 }
852 }
853
854 private void sendStartStream() throws IOException {
855 Tag stream = Tag.start("stream:stream");
856 stream.setAttribute("from", account.getJid());
857 stream.setAttribute("to", account.getServer());
858 stream.setAttribute("version", "1.0");
859 stream.setAttribute("xml:lang", "en");
860 stream.setAttribute("xmlns", "jabber:client");
861 stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
862 tagWriter.writeTag(stream);
863 }
864
865 private String nextRandomId() {
866 return new BigInteger(50, mRandom).toString(32);
867 }
868
869 public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
870 if (packet.getId() == null) {
871 String id = nextRandomId();
872 packet.setAttribute("id", id);
873 }
874 packet.setFrom(account.getFullJid());
875 this.sendPacket(packet, callback);
876 }
877
878 public void sendUnboundIqPacket(IqPacket packet, OnIqPacketReceived callback) {
879 if (packet.getId() == null) {
880 String id = nextRandomId();
881 packet.setAttribute("id", id);
882 }
883 this.sendPacket(packet, callback);
884 }
885
886 public void sendMessagePacket(MessagePacket packet) {
887 this.sendPacket(packet, null);
888 }
889
890 public void sendPresencePacket(PresencePacket packet) {
891 this.sendPacket(packet, null);
892 }
893
894 private synchronized void sendPacket(final AbstractStanza packet,
895 PacketReceived callback) {
896 if (packet.getName().equals("iq") || packet.getName().equals("message")
897 || packet.getName().equals("presence")) {
898 ++stanzasSent;
899 }
900 tagWriter.writeStanzaAsync(packet);
901 if (packet instanceof MessagePacket && packet.getId() != null
902 && this.streamId != null) {
903 Log.d(Config.LOGTAG, "request delivery report for stanza "
904 + stanzasSent);
905 this.messageReceipts.put(stanzasSent, packet.getId());
906 tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion));
907 }
908 if (callback != null) {
909 if (packet.getId() == null) {
910 packet.setId(nextRandomId());
911 }
912 packetCallbacks.put(packet.getId(), callback);
913 }
914 }
915
916 public void sendPing() {
917 if (streamFeatures.hasChild("sm")) {
918 tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
919 } else {
920 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
921 iq.setFrom(account.getFullJid());
922 iq.addChild("ping", "urn:xmpp:ping");
923 this.sendIqPacket(iq, null);
924 }
925 this.lastPingSent = SystemClock.elapsedRealtime();
926 }
927
928 public void setOnMessagePacketReceivedListener(
929 OnMessagePacketReceived listener) {
930 this.messageListener = listener;
931 }
932
933 public void setOnUnregisteredIqPacketReceivedListener(
934 OnIqPacketReceived listener) {
935 this.unregisteredIqListener = listener;
936 }
937
938 public void setOnPresencePacketReceivedListener(
939 OnPresencePacketReceived listener) {
940 this.presenceListener = listener;
941 }
942
943 public void setOnJinglePacketReceivedListener(
944 OnJinglePacketReceived listener) {
945 this.jingleListener = listener;
946 }
947
948 public void setOnStatusChangedListener(OnStatusChanged listener) {
949 this.statusListener = listener;
950 }
951
952 public void setOnBindListener(OnBindListener listener) {
953 this.bindListener = listener;
954 }
955
956 public void setOnMessageAcknowledgeListener(OnMessageAcknowledged listener) {
957 this.acknowledgedListener = listener;
958 }
959
960 public void disconnect(boolean force) {
961 Log.d(Config.LOGTAG, account.getJid() + ": disconnecting");
962 try {
963 if (force) {
964 socket.close();
965 return;
966 }
967 new Thread(new Runnable() {
968
969 @Override
970 public void run() {
971 if (tagWriter.isActive()) {
972 tagWriter.finish();
973 try {
974 while (!tagWriter.finished()) {
975 Log.d(Config.LOGTAG, "not yet finished");
976 Thread.sleep(100);
977 }
978 tagWriter.writeTag(Tag.end("stream:stream"));
979 socket.close();
980 } catch (IOException e) {
981 Log.d(Config.LOGTAG,
982 "io exception during disconnect");
983 } catch (InterruptedException e) {
984 Log.d(Config.LOGTAG, "interrupted");
985 }
986 }
987 }
988 }).start();
989 } catch (IOException e) {
990 Log.d(Config.LOGTAG, "io exception during disconnect");
991 }
992 }
993
994 public List<String> findDiscoItemsByFeature(String feature) {
995 List<String> items = new ArrayList<String>();
996 for (Entry<String, List<String>> cursor : disco.entrySet()) {
997 if (cursor.getValue().contains(feature)) {
998 items.add(cursor.getKey());
999 }
1000 }
1001 return items;
1002 }
1003
1004 public String findDiscoItemByFeature(String feature) {
1005 List<String> items = findDiscoItemsByFeature(feature);
1006 if (items.size() >= 1) {
1007 return items.get(0);
1008 }
1009 return null;
1010 }
1011
1012 public void r() {
1013 this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
1014 }
1015
1016 public String getMucServer() {
1017 return findDiscoItemByFeature("http://jabber.org/protocol/muc");
1018 }
1019
1020 public int getTimeToNextAttempt() {
1021 int interval = (int) (25 * Math.pow(1.5, attempt));
1022 int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
1023 return interval - secondsSinceLast;
1024 }
1025
1026 public int getAttempt() {
1027 return this.attempt;
1028 }
1029
1030 public Features getFeatures() {
1031 return this.features;
1032 }
1033
1034 public class Features {
1035 XmppConnection connection;
1036
1037 public Features(XmppConnection connection) {
1038 this.connection = connection;
1039 }
1040
1041 private boolean hasDiscoFeature(String server, String feature) {
1042 if (!connection.disco.containsKey(server)) {
1043 return false;
1044 }
1045 return connection.disco.get(server).contains(feature);
1046 }
1047
1048 public boolean carbons() {
1049 return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2");
1050 }
1051
1052 public boolean sm() {
1053 return streamId != null;
1054 }
1055
1056 public boolean csi() {
1057 if (connection.streamFeatures == null) {
1058 return false;
1059 } else {
1060 return connection.streamFeatures.hasChild("csi",
1061 "urn:xmpp:csi:0");
1062 }
1063 }
1064
1065 public boolean pubsub() {
1066 return hasDiscoFeature(account.getServer(),
1067 "http://jabber.org/protocol/pubsub#publish");
1068 }
1069
1070 public boolean rosterVersioning() {
1071 if (connection.streamFeatures == null) {
1072 return false;
1073 } else {
1074 return connection.streamFeatures.hasChild("ver");
1075 }
1076 }
1077
1078 public boolean streamhost() {
1079 return connection
1080 .findDiscoItemByFeature("http://jabber.org/protocol/bytestreams") != null;
1081 }
1082
1083 public boolean compression() {
1084 return connection.usingCompression;
1085 }
1086 }
1087
1088 public long getLastSessionEstablished() {
1089 long diff;
1090 if (this.lastSessionStarted == 0) {
1091 diff = SystemClock.elapsedRealtime() - this.lastConnect;
1092 } else {
1093 diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
1094 }
1095 return System.currentTimeMillis() - diff;
1096 }
1097
1098 public long getLastConnect() {
1099 return this.lastConnect;
1100 }
1101
1102 public long getLastPingSent() {
1103 return this.lastPingSent;
1104 }
1105
1106 public long getLastPacketReceived() {
1107 return this.lastPaketReceived;
1108 }
1109
1110 public void sendActive() {
1111 this.sendPacket(new ActivePacket(), null);
1112 }
1113
1114 public void sendInactive() {
1115 this.sendPacket(new InactivePacket(), null);
1116 }
1117}