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