XmppConnection.java

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