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