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