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 if (this.streamFeatures.hasChild("session")) {
546 Log.d(LOGTAG,account.getJid()+": sending deprecated session");
547 IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
548 startSession.addChild("session","urn:ietf:params:xml:ns:xmpp-session"); //setContent("")
549 startSession.setId(nextRandomId());
550 this.sendPacket(startSession, null);
551 }
552 }
553 }
554
555 private boolean compressionAvailable() {
556 if (!this.streamFeatures.hasChild("compression", "http://jabber.org/features/compress")) return false;
557 if (!ZLibOutputStream.SUPPORTED) return false;
558 if (!account.isOptionSet(Account.OPTION_USECOMPRESSION)) return false;
559
560 Element compression = this.streamFeatures.findChild("compression", "http://jabber.org/features/compress");
561 for (Element child : compression.getChildren()) {
562 if (!"method".equals(child.getName())) continue;
563
564 if ("zlib".equalsIgnoreCase(child.getContent())) {
565 return true;
566 }
567 }
568 return false;
569 }
570
571 private List<String> extractMechanisms(Element stream) {
572 ArrayList<String> mechanisms = new ArrayList<String>(stream.getChildren().size());
573 for(Element child : stream.getChildren()) {
574 mechanisms.add(child.getContent());
575 }
576 return mechanisms;
577 }
578
579 private void sendRegistryRequest() {
580 IqPacket register = new IqPacket(IqPacket.TYPE_GET);
581 register.query("jabber:iq:register");
582 register.setTo(account.getServer());
583 sendIqPacket(register, new OnIqPacketReceived() {
584
585 @Override
586 public void onIqPacketReceived(Account account, IqPacket packet) {
587 Element instructions = packet.query().findChild("instructions");
588 if (packet.query().hasChild("username")&&(packet.query().hasChild("password"))) {
589 IqPacket register = new IqPacket(IqPacket.TYPE_SET);
590 Element username = new Element("username").setContent(account.getUsername());
591 Element password = new Element("password").setContent(account.getPassword());
592 register.query("jabber:iq:register").addChild(username);
593 register.query().addChild(password);
594 sendIqPacket(register, new OnIqPacketReceived() {
595
596 @Override
597 public void onIqPacketReceived(Account account, IqPacket packet) {
598 if (packet.getType()==IqPacket.TYPE_RESULT) {
599 account.setOption(Account.OPTION_REGISTER, false);
600 changeStatus(Account.STATUS_REGISTRATION_SUCCESSFULL);
601 } else if (packet.hasChild("error")&&(packet.findChild("error").hasChild("conflict"))){
602 changeStatus(Account.STATUS_REGISTRATION_CONFLICT);
603 } else {
604 changeStatus(Account.STATUS_REGISTRATION_FAILED);
605 Log.d(LOGTAG,packet.toString());
606 }
607 disconnect(true);
608 }
609 });
610 } else {
611 changeStatus(Account.STATUS_REGISTRATION_FAILED);
612 disconnect(true);
613 Log.d(LOGTAG,account.getJid()+": could not register. instructions are"+instructions.getContent());
614 }
615 }
616 });
617 }
618
619 private void sendInitialPresence() {
620 PresencePacket packet = new PresencePacket();
621 packet.setAttribute("from", account.getFullJid());
622 if (account.getKeys().has("pgp_signature")) {
623 try {
624 String signature = account.getKeys().getString("pgp_signature");
625 packet.addChild("status").setContent("online");
626 packet.addChild("x","jabber:x:signed").setContent(signature);
627 } catch (JSONException e) {
628 //
629 }
630 }
631 this.sendPresencePacket(packet);
632 }
633
634 private void sendBindRequest() throws IOException {
635 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
636 iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind").addChild("resource").setContent(account.getResource());
637 this.sendIqPacket(iq, new OnIqPacketReceived() {
638 @Override
639 public void onIqPacketReceived(Account account, IqPacket packet) {
640 String resource = packet.findChild("bind").findChild("jid")
641 .getContent().split("/")[1];
642 account.setResource(resource);
643 if (streamFeatures.hasChild("sm","urn:xmpp:sm:3")) {
644 smVersion = 3;
645 EnablePacket enable = new EnablePacket(smVersion);
646 tagWriter.writeStanzaAsync(enable);
647 } else if (streamFeatures.hasChild("sm","urn:xmpp:sm:2")) {
648 smVersion = 2;
649 EnablePacket enable = new EnablePacket(smVersion);
650 tagWriter.writeStanzaAsync(enable);
651 }
652 sendInitialPresence();
653 sendServiceDiscoveryInfo(account.getServer());
654 sendServiceDiscoveryItems(account.getServer());
655 if (bindListener !=null) {
656 bindListener.onBind(account);
657 }
658 changeStatus(Account.STATUS_ONLINE);
659 }
660 });
661 }
662
663 private void sendServiceDiscoveryInfo(final String server) {
664 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
665 iq.setTo(server);
666 iq.query("http://jabber.org/protocol/disco#info");
667 this.sendIqPacket(iq, new OnIqPacketReceived() {
668
669 @Override
670 public void onIqPacketReceived(Account account, IqPacket packet) {
671 List<Element> elements = packet.query().getChildren();
672 List<String> features = new ArrayList<String>();
673 for (int i = 0; i < elements.size(); ++i) {
674 if (elements.get(i).getName().equals("feature")) {
675 features.add(elements.get(i).getAttribute(
676 "var"));
677 }
678 }
679 disco.put(server, features);
680
681 if (account.getServer().equals(server)) {
682 enableAdvancedStreamFeatures();
683 }
684 }
685 });
686 }
687
688 private void enableAdvancedStreamFeatures() {
689 if (hasFeaturesCarbon()) {
690 sendEnableCarbons();
691 }
692 }
693
694 private void sendServiceDiscoveryItems(final String server) {
695 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
696 iq.setTo(server);
697 iq.query("http://jabber.org/protocol/disco#items");
698 this.sendIqPacket(iq, new OnIqPacketReceived() {
699
700 @Override
701 public void onIqPacketReceived(Account account, IqPacket packet) {
702 List<Element> elements = packet.query().getChildren();
703 for (int i = 0; i < elements.size(); ++i) {
704 if (elements.get(i).getName().equals("item")) {
705 String jid = elements.get(i).getAttribute(
706 "jid");
707 sendServiceDiscoveryInfo(jid);
708 }
709 }
710 }
711 });
712 }
713
714 private void sendEnableCarbons() {
715 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
716 iq.addChild("enable","urn:xmpp:carbons:2");
717 this.sendIqPacket(iq, new OnIqPacketReceived() {
718
719 @Override
720 public void onIqPacketReceived(Account account, IqPacket packet) {
721 if (!packet.hasChild("error")) {
722 Log.d(LOGTAG, account.getJid()
723 + ": successfully enabled carbons");
724 } else {
725 Log.d(LOGTAG, account.getJid()
726 + ": error enableing carbons " + packet.toString());
727 }
728 }
729 });
730 }
731
732 private void processStreamError(Tag currentTag) {
733 Log.d(LOGTAG, "processStreamError");
734 }
735
736 private void sendStartStream() throws IOException {
737 Tag stream = Tag.start("stream:stream");
738 stream.setAttribute("from", account.getJid());
739 stream.setAttribute("to", account.getServer());
740 stream.setAttribute("version", "1.0");
741 stream.setAttribute("xml:lang", "en");
742 stream.setAttribute("xmlns", "jabber:client");
743 stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
744 tagWriter.writeTag(stream);
745 }
746
747 private String nextRandomId() {
748 return new BigInteger(50, random).toString(32);
749 }
750
751 public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
752 if (packet.getId()==null) {
753 String id = nextRandomId();
754 packet.setAttribute("id", id);
755 }
756 packet.setFrom(account.getFullJid());
757 this.sendPacket(packet, callback);
758 }
759
760 public void sendMessagePacket(MessagePacket packet) {
761 this.sendPacket(packet, null);
762 }
763
764 public void sendMessagePacket(MessagePacket packet,
765 OnMessagePacketReceived callback) {
766 this.sendPacket(packet, callback);
767 }
768
769 public void sendPresencePacket(PresencePacket packet) {
770 this.sendPacket(packet, null);
771 }
772
773 public void sendPresencePacket(PresencePacket packet,
774 OnPresencePacketReceived callback) {
775 this.sendPacket(packet, callback);
776 }
777
778 private synchronized void sendPacket(final AbstractStanza packet, PacketReceived callback) {
779 // TODO dont increment stanza count if packet = request packet or ack;
780 ++stanzasSent;
781 tagWriter.writeStanzaAsync(packet);
782 if (callback != null) {
783 if (packet.getId()==null) {
784 packet.setId(nextRandomId());
785 }
786 packetCallbacks.put(packet.getId(), callback);
787 }
788 }
789
790 public void sendPing() {
791 if (streamFeatures.hasChild("sm")) {
792 tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
793 } else {
794 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
795 iq.setFrom(account.getFullJid());
796 iq.addChild("ping","urn:xmpp:ping");
797 this.sendIqPacket(iq, null);
798 }
799 }
800
801 public void setOnMessagePacketReceivedListener(
802 OnMessagePacketReceived listener) {
803 this.messageListener = listener;
804 }
805
806 public void setOnUnregisteredIqPacketReceivedListener(
807 OnIqPacketReceived listener) {
808 this.unregisteredIqListener = listener;
809 }
810
811 public void setOnPresencePacketReceivedListener(
812 OnPresencePacketReceived listener) {
813 this.presenceListener = listener;
814 }
815
816 public void setOnJinglePacketReceivedListener(OnJinglePacketReceived listener) {
817 this.jingleListener = listener;
818 }
819
820 public void setOnStatusChangedListener(OnStatusChanged listener) {
821 this.statusListener = listener;
822 }
823
824 public void setOnTLSExceptionReceivedListener(OnTLSExceptionReceived listener) {
825 this.tlsListener = listener;
826 }
827
828 public void setOnBindListener(OnBindListener listener) {
829 this.bindListener = listener;
830 }
831
832 public void disconnect(boolean force) {
833 changeStatus(Account.STATUS_OFFLINE);
834 Log.d(LOGTAG,"disconnecting");
835 try {
836 if (force) {
837 socket.close();
838 return;
839 }
840 if (tagWriter.isActive()) {
841 tagWriter.finish();
842 while(!tagWriter.finished()) {
843 //Log.d(LOGTAG,"not yet finished");
844 Thread.sleep(100);
845 }
846 tagWriter.writeTag(Tag.end("stream:stream"));
847 }
848 } catch (IOException e) {
849 Log.d(LOGTAG,"io exception during disconnect");
850 } catch (InterruptedException e) {
851 Log.d(LOGTAG,"interupted while waiting for disconnect");
852 }
853 }
854
855 public boolean hasFeatureRosterManagment() {
856 if (this.streamFeatures==null) {
857 return false;
858 } else {
859 return this.streamFeatures.hasChild("ver");
860 }
861 }
862
863 public boolean hasFeatureStreamManagment() {
864 if (this.streamFeatures==null) {
865 return false;
866 } else {
867 return this.streamFeatures.hasChild("sm");
868 }
869 }
870
871 public boolean hasFeaturesCarbon() {
872 return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2");
873 }
874
875 public boolean hasDiscoFeature(String server, String feature) {
876 if (!disco.containsKey(server)) {
877 return false;
878 }
879 return disco.get(server).contains(feature);
880 }
881
882 public String findDiscoItemByFeature(String feature) {
883 Iterator<Entry<String, List<String>>> it = this.disco.entrySet().iterator();
884 while (it.hasNext()) {
885 Entry<String, List<String>> pairs = it.next();
886 if (pairs.getValue().contains(feature)) {
887 return pairs.getKey();
888 }
889 it.remove();
890 }
891 return null;
892 }
893
894 public void r() {
895 this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
896 }
897
898 public int getReceivedStanzas() {
899 return this.stanzasReceived;
900 }
901
902 public int getSentStanzas() {
903 return this.stanzasSent;
904 }
905
906 public String getMucServer() {
907 return findDiscoItemByFeature("http://jabber.org/protocol/muc");
908 }
909
910 public boolean hasPendingSubscription(String jid) {
911 return this.pendingSubscriptions.contains(jid);
912 }
913
914 public void addPendingSubscription(String jid) {
915 Log.d(LOGTAG,"adding "+jid+" to pending subscriptions");
916 this.pendingSubscriptions.add(jid);
917 }
918}