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