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