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