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.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}