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 HashSet<String> pendingSubscriptions = new HashSet<String>();
81
82 private String streamId = null;
83 private int smVersion = 3;
84
85 private int stanzasReceived = 0;
86 private int stanzasSent = 0;
87
88 public long lastPaketReceived = 0;
89 public long lastPingSent = 0;
90 public long lastConnect = 0;
91 public long lastSessionStarted = 0;
92
93 private static final int PACKET_IQ = 0;
94 private static final int PACKET_MESSAGE = 1;
95 private static final int PACKET_PRESENCE = 2;
96
97 private Hashtable<String, PacketReceived> packetCallbacks = new Hashtable<String, PacketReceived>();
98 private OnPresencePacketReceived presenceListener = null;
99 private OnJinglePacketReceived jingleListener = null;
100 private OnIqPacketReceived unregisteredIqListener = null;
101 private OnMessagePacketReceived messageListener = null;
102 private OnStatusChanged statusListener = null;
103 private OnTLSExceptionReceived tlsListener = null;
104 private OnBindListener bindListener = null;
105
106 public XmppConnection(Account account, PowerManager pm) {
107 this.account = account;
108 this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,account.getJid());
109 tagWriter = new TagWriter();
110 }
111
112 protected void changeStatus(int nextStatus) {
113 if (account.getStatus() != nextStatus) {
114 if ((nextStatus == Account.STATUS_OFFLINE)&&(account.getStatus() != Account.STATUS_CONNECTING)&&(account.getStatus() != Account.STATUS_ONLINE)&&(account.getStatus() != Account.STATUS_DISABLED)) {
115 return;
116 }
117 account.setStatus(nextStatus);
118 if (statusListener != null) {
119 statusListener.onStatusChanged(account);
120 }
121 }
122 }
123
124 protected void connect() {
125 Log.d(LOGTAG,account.getJid()+ ": connecting");
126 lastConnect = SystemClock.elapsedRealtime();
127 try {
128 shouldAuthenticate = shouldBind = !account.isOptionSet(Account.OPTION_REGISTER);
129 tagReader = new XmlReader(wakeLock);
130 tagWriter = new TagWriter();
131 packetCallbacks.clear();
132 this.changeStatus(Account.STATUS_CONNECTING);
133 Bundle namePort = DNSHelper.getSRVRecord(account.getServer());
134 if ("timeout".equals(namePort.getString("error"))) {
135 Log.d(LOGTAG,account.getJid()+": dns timeout");
136 this.changeStatus(Account.STATUS_OFFLINE);
137 return;
138 }
139 String srvRecordServer = namePort.getString("name");
140 String srvIpServer = namePort.getString("ipv4");
141 int srvRecordPort = namePort.getInt("port");
142 if (srvRecordServer != null) {
143 if (srvIpServer != null) {
144 Log.d(LOGTAG, account.getJid() + ": using values from dns "
145 + srvRecordServer + "[" + srvIpServer + "]:"
146 + srvRecordPort);
147 socket = new Socket(srvIpServer, srvRecordPort);
148 } else {
149 Log.d(LOGTAG, account.getJid() + ": using values from dns "
150 + srvRecordServer + ":" + srvRecordPort);
151 socket = new Socket(srvRecordServer, srvRecordPort);
152 }
153 } else {
154 socket = new Socket(account.getServer(), 5222);
155 }
156 OutputStream out = socket.getOutputStream();
157 tagWriter.setOutputStream(out);
158 InputStream in = socket.getInputStream();
159 tagReader.setInputStream(in);
160 tagWriter.beginDocument();
161 sendStartStream();
162 Tag nextTag;
163 while ((nextTag = tagReader.readTag()) != null) {
164 if (nextTag.isStart("stream")) {
165 processStream(nextTag);
166 break;
167 } else {
168 Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName());
169 return;
170 }
171 }
172 if (socket.isConnected()) {
173 socket.close();
174 }
175 } catch (UnknownHostException e) {
176 this.changeStatus(Account.STATUS_SERVER_NOT_FOUND);
177 if (wakeLock.isHeld()) {
178 wakeLock.release();
179 }
180 return;
181 } catch (IOException e) {
182 if (account.getStatus() != Account.STATUS_TLS_ERROR) {
183 this.changeStatus(Account.STATUS_OFFLINE);
184 }
185 if (wakeLock.isHeld()) {
186 wakeLock.release();
187 }
188 return;
189 } catch (NoSuchAlgorithmException e) {
190 this.changeStatus(Account.STATUS_OFFLINE);
191 Log.d(LOGTAG, "compression exception " + e.getMessage());
192 if (wakeLock.isHeld()) {
193 wakeLock.release();
194 }
195 return;
196 } catch (XmlPullParserException e) {
197 this.changeStatus(Account.STATUS_OFFLINE);
198 Log.d(LOGTAG, "xml exception " + e.getMessage());
199 if (wakeLock.isHeld()) {
200 wakeLock.release();
201 }
202 return;
203 }
204
205 }
206
207 @Override
208 public void run() {
209 connect();
210 }
211
212 private void processStream(Tag currentTag) throws XmlPullParserException,
213 IOException, NoSuchAlgorithmException {
214 Tag nextTag = tagReader.readTag();
215 while ((nextTag != null) && (!nextTag.isEnd("stream"))) {
216 if (nextTag.isStart("error")) {
217 processStreamError(nextTag);
218 } else if (nextTag.isStart("features")) {
219 processStreamFeatures(nextTag);
220 if ((streamFeatures.getChildren().size() == 1)
221 && (streamFeatures.hasChild("starttls"))
222 && (!account.isOptionSet(Account.OPTION_USETLS))) {
223 changeStatus(Account.STATUS_SERVER_REQUIRES_TLS);
224 }
225 } else if (nextTag.isStart("proceed")) {
226 switchOverToTls(nextTag);
227 } else if (nextTag.isStart("compressed")) {
228 switchOverToZLib(nextTag);
229 } else if (nextTag.isStart("success")) {
230 Log.d(LOGTAG, account.getJid()
231 + ": logged in");
232 tagReader.readTag();
233 tagReader.reset();
234 sendStartStream();
235 processStream(tagReader.readTag());
236 break;
237 } else if (nextTag.isStart("failure")) {
238 Element failure = tagReader.readElement(nextTag);
239 changeStatus(Account.STATUS_UNAUTHORIZED);
240 } else if (nextTag.isStart("challenge")) {
241 String challange = tagReader.readElement(nextTag).getContent();
242 Element response = new Element("response");
243 response.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
244 response.setContent(CryptoHelper.saslDigestMd5(account, challange));
245 tagWriter.writeElement(response);
246 } else if (nextTag.isStart("enabled")) {
247 this.stanzasSent = 0;
248 Element enabled = tagReader.readElement(nextTag);
249 if ("true".equals(enabled.getAttribute("resume"))) {
250 this.streamId = enabled.getAttribute("id");
251 Log.d(LOGTAG,account.getJid()+": stream managment("+smVersion+") enabled (resumable)");
252 } else {
253 Log.d(LOGTAG,account.getJid()+": stream managment("+smVersion+") enabled");
254 }
255 this.lastSessionStarted = SystemClock.elapsedRealtime();
256 this.stanzasReceived = 0;
257 RequestPacket r = new RequestPacket(smVersion);
258 tagWriter.writeStanzaAsync(r);
259 } else if (nextTag.isStart("resumed")) {
260 Log.d(LOGTAG,account.getJid()+": session resumed");
261 tagReader.readElement(nextTag);
262 sendPing();
263 changeStatus(Account.STATUS_ONLINE);
264 } else if (nextTag.isStart("r")) {
265 tagReader.readElement(nextTag);
266 AckPacket ack = new AckPacket(this.stanzasReceived,smVersion);
267 //Log.d(LOGTAG,ack.toString());
268 tagWriter.writeStanzaAsync(ack);
269 } else if (nextTag.isStart("a")) {
270 Element ack = tagReader.readElement(nextTag);
271 lastPaketReceived = SystemClock.elapsedRealtime();
272 int serverSequence = Integer.parseInt(ack.getAttribute("h"));
273 if (serverSequence>this.stanzasSent) {
274 this.stanzasSent = serverSequence;
275 }
276 //Log.d(LOGTAG,"server ack"+ack.toString()+" ("+this.stanzasSent+")");
277 } else if (nextTag.isStart("failed")) {
278 tagReader.readElement(nextTag);
279 Log.d(LOGTAG,account.getJid()+": resumption failed");
280 streamId = null;
281 if (account.getStatus() != Account.STATUS_ONLINE) {
282 sendBindRequest();
283 }
284 } else if (nextTag.isStart("iq")) {
285 processIq(nextTag);
286 } else if (nextTag.isStart("message")) {
287 processMessage(nextTag);
288 } else if (nextTag.isStart("presence")) {
289 processPresence(nextTag);
290 } else {
291 Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName()
292 + " as child of " + currentTag.getName());
293 }
294 nextTag = tagReader.readTag();
295 }
296 if (account.getStatus() == Account.STATUS_ONLINE) {
297 account.setStatus(Account.STATUS_OFFLINE);
298 if (statusListener != null) {
299 statusListener.onStatusChanged(account);
300 }
301 }
302 }
303
304 private Element processPacket(Tag currentTag, int packetType)
305 throws XmlPullParserException, IOException {
306 Element element;
307 switch (packetType) {
308 case PACKET_IQ:
309 element = new IqPacket();
310 break;
311 case PACKET_MESSAGE:
312 element = new MessagePacket();
313 break;
314 case PACKET_PRESENCE:
315 element = new PresencePacket();
316 break;
317 default:
318 return null;
319 }
320 element.setAttributes(currentTag.getAttributes());
321 Tag nextTag = tagReader.readTag();
322 while (!nextTag.isEnd(element.getName())) {
323 if (!nextTag.isNo()) {
324 Element child = tagReader.readElement(nextTag);
325 if ((packetType == PACKET_IQ)&&("jingle".equals(child.getName()))) {
326 element = new JinglePacket();
327 element.setAttributes(currentTag.getAttributes());
328 }
329 element.addChild(child);
330 }
331 nextTag = tagReader.readTag();
332 }
333 ++stanzasReceived;
334 lastPaketReceived = SystemClock.elapsedRealtime();
335 return element;
336 }
337
338 private void processIq(Tag currentTag) throws XmlPullParserException,
339 IOException {
340 IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
341
342 if (packet.getId() == null) {
343 return; //an iq packet without id is definitely invalid
344 }
345
346 if (packet instanceof JinglePacket) {
347 if (this.jingleListener !=null) {
348 this.jingleListener.onJinglePacketReceived(account, (JinglePacket) packet);
349 }
350 } else {
351 if (packetCallbacks.containsKey(packet.getId())) {
352 if (packetCallbacks.get(packet.getId()) instanceof OnIqPacketReceived) {
353 ((OnIqPacketReceived) packetCallbacks.get(packet.getId()))
354 .onIqPacketReceived(account, packet);
355 }
356
357 packetCallbacks.remove(packet.getId());
358 } else if (this.unregisteredIqListener != null) {
359 this.unregisteredIqListener.onIqPacketReceived(account, packet);
360 }
361 }
362 }
363
364 private void processMessage(Tag currentTag) throws XmlPullParserException,
365 IOException {
366 MessagePacket packet = (MessagePacket) processPacket(currentTag,
367 PACKET_MESSAGE);
368 String id = packet.getAttribute("id");
369 if ((id != null) && (packetCallbacks.containsKey(id))) {
370 if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) {
371 ((OnMessagePacketReceived) packetCallbacks.get(id))
372 .onMessagePacketReceived(account, packet);
373 }
374 packetCallbacks.remove(id);
375 } else if (this.messageListener != null) {
376 this.messageListener.onMessagePacketReceived(account, packet);
377 }
378 }
379
380 private void processPresence(Tag currentTag) throws XmlPullParserException,
381 IOException {
382 PresencePacket packet = (PresencePacket) processPacket(currentTag,
383 PACKET_PRESENCE);
384 String id = packet.getAttribute("id");
385 if ((id != null) && (packetCallbacks.containsKey(id))) {
386 if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) {
387 ((OnPresencePacketReceived) packetCallbacks.get(id))
388 .onPresencePacketReceived(account, packet);
389 }
390 packetCallbacks.remove(id);
391 } else if (this.presenceListener != null) {
392 this.presenceListener.onPresencePacketReceived(account, packet);
393 }
394 }
395
396 private void sendCompressionZlib() throws IOException {
397 Element compress = new Element("compress");
398 compress.setAttribute("xmlns", "http://jabber.org/protocol/compress");
399 compress.addChild("method").setContent("zlib");
400 tagWriter.writeElement(compress);
401 }
402
403 private void switchOverToZLib(Tag currentTag) throws XmlPullParserException,
404 IOException, NoSuchAlgorithmException {
405 tagReader.readTag(); // read tag close
406
407 tagWriter.setOutputStream(new ZLibOutputStream(tagWriter.getOutputStream()));
408 tagReader.setInputStream(new ZLibInputStream(tagReader.getInputStream()));
409
410 sendStartStream();
411 Log.d(LOGTAG,account.getJid()+": compression enabled");
412 processStream(tagReader.readTag());
413 }
414
415 private void sendStartTLS() throws IOException {
416 Tag startTLS = Tag.empty("starttls");
417 startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
418 tagWriter.writeTag(startTLS);
419 }
420
421 private void switchOverToTls(Tag currentTag) throws XmlPullParserException,
422 IOException {
423 Tag nextTag = tagReader.readTag(); // should be proceed end tag
424 try {
425 SSLContext sc = SSLContext.getInstance("TLS");
426 TrustManagerFactory tmf = TrustManagerFactory
427 .getInstance(TrustManagerFactory.getDefaultAlgorithm());
428 // Initialise the TMF as you normally would, for example:
429 // tmf.in
430 try {
431 tmf.init((KeyStore) null);
432 } catch (KeyStoreException e1) {
433 // TODO Auto-generated catch block
434 e1.printStackTrace();
435 }
436
437 TrustManager[] trustManagers = tmf.getTrustManagers();
438 final X509TrustManager origTrustmanager = (X509TrustManager) trustManagers[0];
439
440 TrustManager[] wrappedTrustManagers = new TrustManager[] { new X509TrustManager() {
441
442 @Override
443 public void checkClientTrusted(X509Certificate[] chain,
444 String authType) throws CertificateException {
445 origTrustmanager.checkClientTrusted(chain, authType);
446 }
447
448 @Override
449 public void checkServerTrusted(X509Certificate[] chain,
450 String authType) throws CertificateException {
451 try {
452 origTrustmanager.checkServerTrusted(chain, authType);
453 } catch (CertificateException e) {
454 if (e.getCause() instanceof CertPathValidatorException) {
455 String sha;
456 try {
457 MessageDigest sha1 = MessageDigest.getInstance("SHA1");
458 sha1.update(chain[0].getEncoded());
459 sha = CryptoHelper.bytesToHex(sha1.digest());
460 if (!sha.equals(account.getSSLFingerprint())) {
461 changeStatus(Account.STATUS_TLS_ERROR);
462 if (tlsListener!=null) {
463 tlsListener.onTLSExceptionReceived(sha,account);
464 }
465 throw new CertificateException();
466 }
467 } catch (NoSuchAlgorithmException e1) {
468 // TODO Auto-generated catch block
469 e1.printStackTrace();
470 }
471 } else {
472 throw new CertificateException();
473 }
474 }
475 }
476
477 @Override
478 public X509Certificate[] getAcceptedIssuers() {
479 return origTrustmanager.getAcceptedIssuers();
480 }
481
482 } };
483 sc.init(null, wrappedTrustManagers, null);
484 SSLSocketFactory factory = sc.getSocketFactory();
485 SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,
486 socket.getInetAddress().getHostAddress(), socket.getPort(),
487 true);
488 tagReader.setInputStream(sslSocket.getInputStream());
489 tagWriter.setOutputStream(sslSocket.getOutputStream());
490 sendStartStream();
491 Log.d(LOGTAG,account.getJid()+": TLS connection established");
492 processStream(tagReader.readTag());
493 sslSocket.close();
494 } catch (NoSuchAlgorithmException e1) {
495 // TODO Auto-generated catch block
496 e1.printStackTrace();
497 } catch (KeyManagementException e) {
498 // TODO Auto-generated catch block
499 e.printStackTrace();
500 }
501 }
502
503 private void sendSaslAuthPlain() throws IOException {
504 String saslString = CryptoHelper.saslPlain(account.getUsername(),
505 account.getPassword());
506 Element auth = new Element("auth");
507 auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
508 auth.setAttribute("mechanism", "PLAIN");
509 auth.setContent(saslString);
510 tagWriter.writeElement(auth);
511 }
512
513 private void sendSaslAuthDigestMd5() throws IOException {
514 Element auth = new Element("auth");
515 auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
516 auth.setAttribute("mechanism", "DIGEST-MD5");
517 tagWriter.writeElement(auth);
518 }
519
520 private void processStreamFeatures(Tag currentTag)
521 throws XmlPullParserException, IOException {
522 this.streamFeatures = tagReader.readElement(currentTag);
523 if (this.streamFeatures.hasChild("starttls")
524 && account.isOptionSet(Account.OPTION_USETLS)) {
525 sendStartTLS();
526 } else if (compressionAvailable()) {
527 sendCompressionZlib();
528 } else if (this.streamFeatures.hasChild("register")&&(account.isOptionSet(Account.OPTION_REGISTER))) {
529 sendRegistryRequest();
530 } else if (!this.streamFeatures.hasChild("register")&&(account.isOptionSet(Account.OPTION_REGISTER))) {
531 changeStatus(Account.STATUS_REGISTRATION_NOT_SUPPORTED);
532 disconnect(true);
533 } else if (this.streamFeatures.hasChild("mechanisms")
534 && shouldAuthenticate) {
535 List<String> mechanisms = extractMechanisms( streamFeatures.findChild("mechanisms"));
536 if (mechanisms.contains("PLAIN")) {
537 sendSaslAuthPlain();
538 } else if (mechanisms.contains("DIGEST-MD5")) {
539 sendSaslAuthDigestMd5();
540 }
541 } else if (this.streamFeatures.hasChild("sm","urn:xmpp:sm:"+smVersion) && streamId != null) {
542 ResumePacket resume = new ResumePacket(this.streamId,stanzasReceived,smVersion);
543 this.tagWriter.writeStanzaAsync(resume);
544 } else if (this.streamFeatures.hasChild("bind") && shouldBind) {
545 sendBindRequest();
546 if (this.streamFeatures.hasChild("session")) {
547 Log.d(LOGTAG,account.getJid()+": sending deprecated session");
548 IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
549 startSession.addChild("session","urn:ietf:params:xml:ns:xmpp-session"); //setContent("")
550 this.sendIqPacket(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}