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