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.NoSuchAlgorithmException;
  11import java.security.SecureRandom;
  12import java.util.ArrayList;
  13import java.util.Arrays;
  14import java.util.HashMap;
  15import java.util.Hashtable;
  16import java.util.LinkedList;
  17import java.util.List;
  18import java.util.Map.Entry;
  19
  20import javax.net.ssl.HostnameVerifier;
  21import javax.net.ssl.SSLContext;
  22import javax.net.ssl.SSLSocket;
  23import javax.net.ssl.SSLSocketFactory;
  24
  25import javax.net.ssl.X509TrustManager;
  26
  27import org.xmlpull.v1.XmlPullParserException;
  28
  29import de.duenndns.ssl.MemorizingTrustManager;
  30
  31import android.content.Context;
  32import android.content.SharedPreferences;
  33import android.os.Bundle;
  34import android.os.PowerManager;
  35import android.os.PowerManager.WakeLock;
  36import android.os.SystemClock;
  37import android.preference.PreferenceManager;
  38import android.util.Log;
  39import android.util.SparseArray;
  40import eu.siacs.conversations.Config;
  41import eu.siacs.conversations.entities.Account;
  42import eu.siacs.conversations.services.XmppConnectionService;
  43import eu.siacs.conversations.ui.StartConversationActivity;
  44import eu.siacs.conversations.utils.CryptoHelper;
  45import eu.siacs.conversations.utils.DNSHelper;
  46import eu.siacs.conversations.utils.zlib.ZLibOutputStream;
  47import eu.siacs.conversations.utils.zlib.ZLibInputStream;
  48import eu.siacs.conversations.xml.Element;
  49import eu.siacs.conversations.xml.Tag;
  50import eu.siacs.conversations.xml.TagWriter;
  51import eu.siacs.conversations.xml.XmlReader;
  52import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
  53import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
  54import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
  55import eu.siacs.conversations.xmpp.stanzas.IqPacket;
  56import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
  57import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
  58import eu.siacs.conversations.xmpp.stanzas.csi.ActivePacket;
  59import eu.siacs.conversations.xmpp.stanzas.csi.InactivePacket;
  60import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
  61import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
  62import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
  63import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
  64
  65public class XmppConnection implements Runnable {
  66
  67	protected Account account;
  68
  69	private WakeLock wakeLock;
  70
  71	private SecureRandom mRandom;
  72
  73	private Socket socket;
  74	private XmlReader tagReader;
  75	private TagWriter tagWriter;
  76
  77	private Features features = new Features(this);
  78
  79	private boolean shouldBind = true;
  80	private boolean shouldAuthenticate = true;
  81	private Element streamFeatures;
  82	private HashMap<String, List<String>> disco = new HashMap<String, List<String>>();
  83
  84	private String streamId = null;
  85	private int smVersion = 3;
  86	private SparseArray<String> messageReceipts = new SparseArray<String>();
  87
  88	private boolean usingCompression = false;
  89	private boolean usingEncryption = false;
  90
  91	private int stanzasReceived = 0;
  92	private int stanzasSent = 0;
  93
  94	private long lastPaketReceived = 0;
  95	private long lastPingSent = 0;
  96	private long lastConnect = 0;
  97	private long lastSessionStarted = 0;
  98
  99	private int attempt = 0;
 100
 101	private static final int PACKET_IQ = 0;
 102	private static final int PACKET_MESSAGE = 1;
 103	private static final int PACKET_PRESENCE = 2;
 104
 105	private Hashtable<String, PacketReceived> packetCallbacks = new Hashtable<String, PacketReceived>();
 106	private OnPresencePacketReceived presenceListener = null;
 107	private OnJinglePacketReceived jingleListener = null;
 108	private OnIqPacketReceived unregisteredIqListener = null;
 109	private OnMessagePacketReceived messageListener = null;
 110	private OnStatusChanged statusListener = null;
 111	private OnBindListener bindListener = null;
 112	private OnMessageAcknowledged acknowledgedListener = null;
 113	private MemorizingTrustManager mMemorizingTrustManager;
 114    private final Context applicationContext;
 115
 116	public XmppConnection(Account account, XmppConnectionService service) {
 117		this.mRandom = service.getRNG();
 118		this.mMemorizingTrustManager = service.getMemorizingTrustManager();
 119		this.account = account;
 120		this.wakeLock = service.getPowerManager().newWakeLock(
 121				PowerManager.PARTIAL_WAKE_LOCK, account.getJid());
 122		tagWriter = new TagWriter();
 123        applicationContext = service.getApplicationContext();
 124	}
 125
 126	protected void changeStatus(int nextStatus) {
 127		if (account.getStatus() != nextStatus) {
 128			if ((nextStatus == Account.STATUS_OFFLINE)
 129					&& (account.getStatus() != Account.STATUS_CONNECTING)
 130					&& (account.getStatus() != Account.STATUS_ONLINE)
 131					&& (account.getStatus() != Account.STATUS_DISABLED)) {
 132				return;
 133			}
 134			if (nextStatus == Account.STATUS_ONLINE) {
 135				this.attempt = 0;
 136			}
 137			account.setStatus(nextStatus);
 138			if (statusListener != null) {
 139				statusListener.onStatusChanged(account);
 140			}
 141		}
 142	}
 143
 144	protected void connect() {
 145		Log.d(Config.LOGTAG, account.getJid() + ": connecting");
 146		usingCompression = false;
 147		usingEncryption = false;
 148		lastConnect = SystemClock.elapsedRealtime();
 149		lastPingSent = SystemClock.elapsedRealtime();
 150		this.attempt++;
 151		try {
 152			shouldAuthenticate = shouldBind = !account
 153					.isOptionSet(Account.OPTION_REGISTER);
 154			tagReader = new XmlReader(wakeLock);
 155			tagWriter = new TagWriter();
 156			packetCallbacks.clear();
 157			this.changeStatus(Account.STATUS_CONNECTING);
 158			Bundle namePort = DNSHelper.getSRVRecord(account.getServer());
 159			if ("timeout".equals(namePort.getString("error"))) {
 160				Log.d(Config.LOGTAG, account.getJid() + ": dns timeout");
 161				this.changeStatus(Account.STATUS_OFFLINE);
 162				return;
 163			}
 164			String srvRecordServer = namePort.getString("name");
 165			String srvIpServer = namePort.getString("ipv4");
 166			int srvRecordPort = namePort.getInt("port");
 167			if (srvRecordServer != null) {
 168				if (srvIpServer != null) {
 169					Log.d(Config.LOGTAG, account.getJid()
 170							+ ": using values from dns " + srvRecordServer
 171							+ "[" + srvIpServer + "]:" + srvRecordPort);
 172					socket = new Socket(srvIpServer, srvRecordPort);
 173				} else {
 174					boolean socketError = true;
 175					int srvIndex = 0;
 176					while (socketError && namePort.containsKey("name" + srvIndex)){
 177						try {
 178							srvRecordServer = namePort.getString("name" + srvIndex);
 179							srvRecordPort = namePort.getInt("port" + srvIndex);
 180							Log.d(Config.LOGTAG, account.getJid()
 181									+ ": using values from dns " + srvRecordServer
 182									+ ":" + srvRecordPort);
 183							socket = new Socket(srvRecordServer, srvRecordPort);
 184							socketError = false;
 185						} catch (UnknownHostException e) {
 186							srvIndex++;
 187						} catch (IOException e) {
 188							srvIndex++;
 189						}
 190					}
 191				}
 192			} else if (namePort.containsKey("error")
 193					&& "nosrv".equals(namePort.getString("error", null))) {
 194				socket = new Socket(account.getServer(), 5222);
 195			} else {
 196				Log.d(Config.LOGTAG, account.getJid()
 197						+ ": timeout in DNS resolution");
 198				changeStatus(Account.STATUS_OFFLINE);
 199				return;
 200			}
 201			OutputStream out = socket.getOutputStream();
 202			tagWriter.setOutputStream(out);
 203			InputStream in = socket.getInputStream();
 204			tagReader.setInputStream(in);
 205			tagWriter.beginDocument();
 206			sendStartStream();
 207			Tag nextTag;
 208			while ((nextTag = tagReader.readTag()) != null) {
 209				if (nextTag.isStart("stream")) {
 210					processStream(nextTag);
 211					break;
 212				} else {
 213					Log.d(Config.LOGTAG,
 214							"found unexpected tag: " + nextTag.getName());
 215					return;
 216				}
 217			}
 218			if (socket.isConnected()) {
 219				socket.close();
 220			}
 221		} catch (UnknownHostException e) {
 222			this.changeStatus(Account.STATUS_SERVER_NOT_FOUND);
 223			if (wakeLock.isHeld()) {
 224				try {
 225					wakeLock.release();
 226				} catch (RuntimeException re) {
 227				}
 228			}
 229			return;
 230		} catch (IOException e) {
 231			this.changeStatus(Account.STATUS_OFFLINE);
 232			if (wakeLock.isHeld()) {
 233				try {
 234					wakeLock.release();
 235				} catch (RuntimeException re) {
 236				}
 237			}
 238			return;
 239		} catch (NoSuchAlgorithmException e) {
 240			this.changeStatus(Account.STATUS_OFFLINE);
 241			Log.d(Config.LOGTAG, "compression exception " + e.getMessage());
 242			if (wakeLock.isHeld()) {
 243				try {
 244					wakeLock.release();
 245				} catch (RuntimeException re) {
 246				}
 247			}
 248			return;
 249		} catch (XmlPullParserException e) {
 250			this.changeStatus(Account.STATUS_OFFLINE);
 251			Log.d(Config.LOGTAG, "xml exception " + e.getMessage());
 252			if (wakeLock.isHeld()) {
 253				try {
 254					wakeLock.release();
 255				} catch (RuntimeException re) {
 256				}
 257			}
 258			return;
 259		}
 260
 261	}
 262
 263	@Override
 264	public void run() {
 265		connect();
 266	}
 267
 268	private void processStream(Tag currentTag) throws XmlPullParserException,
 269			IOException, NoSuchAlgorithmException {
 270		Tag nextTag = tagReader.readTag();
 271		while ((nextTag != null) && (!nextTag.isEnd("stream"))) {
 272			if (nextTag.isStart("error")) {
 273				processStreamError(nextTag);
 274			} else if (nextTag.isStart("features")) {
 275				processStreamFeatures(nextTag);
 276			} else if (nextTag.isStart("proceed")) {
 277				switchOverToTls(nextTag);
 278			} else if (nextTag.isStart("compressed")) {
 279				switchOverToZLib(nextTag);
 280			} else if (nextTag.isStart("success")) {
 281				Log.d(Config.LOGTAG, account.getJid() + ": logged in");
 282				tagReader.readTag();
 283				tagReader.reset();
 284				sendStartStream();
 285				processStream(tagReader.readTag());
 286				break;
 287			} else if (nextTag.isStart("failure")) {
 288				tagReader.readElement(nextTag);
 289				changeStatus(Account.STATUS_UNAUTHORIZED);
 290			} else if (nextTag.isStart("challenge")) {
 291				String challange = tagReader.readElement(nextTag).getContent();
 292				Element response = new Element("response");
 293				response.setAttribute("xmlns",
 294						"urn:ietf:params:xml:ns:xmpp-sasl");
 295				response.setContent(CryptoHelper.saslDigestMd5(account,
 296						challange, mRandom));
 297				tagWriter.writeElement(response);
 298			} else if (nextTag.isStart("enabled")) {
 299				Element enabled = tagReader.readElement(nextTag);
 300				if ("true".equals(enabled.getAttribute("resume"))) {
 301					this.streamId = enabled.getAttribute("id");
 302					Log.d(Config.LOGTAG, account.getJid()
 303							+ ": stream managment(" + smVersion
 304							+ ") enabled (resumable)");
 305				} else {
 306					Log.d(Config.LOGTAG, account.getJid()
 307							+ ": stream managment(" + smVersion + ") enabled");
 308				}
 309				this.lastSessionStarted = SystemClock.elapsedRealtime();
 310				this.stanzasReceived = 0;
 311				RequestPacket r = new RequestPacket(smVersion);
 312				tagWriter.writeStanzaAsync(r);
 313			} else if (nextTag.isStart("resumed")) {
 314				lastPaketReceived = SystemClock.elapsedRealtime();
 315				Element resumed = tagReader.readElement(nextTag);
 316				String h = resumed.getAttribute("h");
 317				try {
 318					int serverCount = Integer.parseInt(h);
 319					if (serverCount != stanzasSent) {
 320						Log.d(Config.LOGTAG, account.getJid()
 321								+ ": session resumed with lost packages");
 322						stanzasSent = serverCount;
 323					} else {
 324						Log.d(Config.LOGTAG, account.getJid()
 325								+ ": session resumed");
 326					}
 327					if (acknowledgedListener != null) {
 328						for (int i = 0; i < messageReceipts.size(); ++i) {
 329							if (serverCount >= messageReceipts.keyAt(i)) {
 330								acknowledgedListener.onMessageAcknowledged(
 331										account, messageReceipts.valueAt(i));
 332							}
 333						}
 334					}
 335					messageReceipts.clear();
 336				} catch (NumberFormatException e) {
 337
 338				}
 339				sendInitialPing();
 340
 341			} else if (nextTag.isStart("r")) {
 342				tagReader.readElement(nextTag);
 343				AckPacket ack = new AckPacket(this.stanzasReceived, smVersion);
 344				tagWriter.writeStanzaAsync(ack);
 345			} else if (nextTag.isStart("a")) {
 346				Element ack = tagReader.readElement(nextTag);
 347				lastPaketReceived = SystemClock.elapsedRealtime();
 348				int serverSequence = Integer.parseInt(ack.getAttribute("h"));
 349				String msgId = this.messageReceipts.get(serverSequence);
 350				if (msgId != null) {
 351					if (this.acknowledgedListener != null) {
 352						this.acknowledgedListener.onMessageAcknowledged(
 353								account, msgId);
 354					}
 355					this.messageReceipts.remove(serverSequence);
 356				}
 357			} else if (nextTag.isStart("failed")) {
 358				tagReader.readElement(nextTag);
 359				Log.d(Config.LOGTAG, account.getJid() + ": resumption failed");
 360				streamId = null;
 361				if (account.getStatus() != Account.STATUS_ONLINE) {
 362					sendBindRequest();
 363				}
 364			} else if (nextTag.isStart("iq")) {
 365				processIq(nextTag);
 366			} else if (nextTag.isStart("message")) {
 367				processMessage(nextTag);
 368			} else if (nextTag.isStart("presence")) {
 369				processPresence(nextTag);
 370			}
 371			nextTag = tagReader.readTag();
 372		}
 373		if (account.getStatus() == Account.STATUS_ONLINE) {
 374			account.setStatus(Account.STATUS_OFFLINE);
 375			if (statusListener != null) {
 376				statusListener.onStatusChanged(account);
 377			}
 378		}
 379	}
 380
 381	private void sendInitialPing() {
 382		Log.d(Config.LOGTAG, account.getJid() + ": sending intial ping");
 383		IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
 384		iq.setFrom(account.getFullJid());
 385		iq.addChild("ping", "urn:xmpp:ping");
 386		this.sendIqPacket(iq, new OnIqPacketReceived() {
 387
 388            @Override
 389            public void onIqPacketReceived(Account account, IqPacket packet) {
 390                Log.d(Config.LOGTAG, account.getJid()
 391                        + ": online with resource " + account.getResource());
 392                changeStatus(Account.STATUS_ONLINE);
 393            }
 394        });
 395	}
 396
 397	private Element processPacket(Tag currentTag, int packetType)
 398			throws XmlPullParserException, IOException {
 399		Element element;
 400		switch (packetType) {
 401		case PACKET_IQ:
 402			element = new IqPacket();
 403			break;
 404		case PACKET_MESSAGE:
 405			element = new MessagePacket();
 406			break;
 407		case PACKET_PRESENCE:
 408			element = new PresencePacket();
 409			break;
 410		default:
 411			return null;
 412		}
 413		element.setAttributes(currentTag.getAttributes());
 414		Tag nextTag = tagReader.readTag();
 415		if (nextTag == null) {
 416			throw new IOException("interrupted mid tag");
 417		}
 418		while (!nextTag.isEnd(element.getName())) {
 419			if (!nextTag.isNo()) {
 420				Element child = tagReader.readElement(nextTag);
 421				String type = currentTag.getAttribute("type");
 422				if (packetType == PACKET_IQ
 423						&& "jingle".equals(child.getName())
 424						&& ("set".equalsIgnoreCase(type) || "get"
 425								.equalsIgnoreCase(type))) {
 426					element = new JinglePacket();
 427					element.setAttributes(currentTag.getAttributes());
 428				}
 429				element.addChild(child);
 430			}
 431			nextTag = tagReader.readTag();
 432			if (nextTag == null) {
 433				throw new IOException("interrupted mid tag");
 434			}
 435		}
 436		++stanzasReceived;
 437		lastPaketReceived = SystemClock.elapsedRealtime();
 438		return element;
 439	}
 440
 441	private void processIq(Tag currentTag) throws XmlPullParserException,
 442			IOException {
 443		IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
 444
 445		if (packet.getId() == null) {
 446			return; // an iq packet without id is definitely invalid
 447		}
 448
 449		if (packet instanceof JinglePacket) {
 450			if (this.jingleListener != null) {
 451				this.jingleListener.onJinglePacketReceived(account,
 452						(JinglePacket) packet);
 453			}
 454		} else {
 455			if (packetCallbacks.containsKey(packet.getId())) {
 456				if (packetCallbacks.get(packet.getId()) instanceof OnIqPacketReceived) {
 457					((OnIqPacketReceived) packetCallbacks.get(packet.getId()))
 458							.onIqPacketReceived(account, packet);
 459				}
 460
 461				packetCallbacks.remove(packet.getId());
 462			} else if ((packet.getType() == IqPacket.TYPE_GET || packet
 463					.getType() == IqPacket.TYPE_SET)
 464					&& this.unregisteredIqListener != null) {
 465				this.unregisteredIqListener.onIqPacketReceived(account, packet);
 466			}
 467		}
 468	}
 469
 470	private void processMessage(Tag currentTag) throws XmlPullParserException,
 471			IOException {
 472		MessagePacket packet = (MessagePacket) processPacket(currentTag,
 473				PACKET_MESSAGE);
 474		String id = packet.getAttribute("id");
 475		if ((id != null) && (packetCallbacks.containsKey(id))) {
 476			if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) {
 477				((OnMessagePacketReceived) packetCallbacks.get(id))
 478						.onMessagePacketReceived(account, packet);
 479			}
 480			packetCallbacks.remove(id);
 481		} else if (this.messageListener != null) {
 482			this.messageListener.onMessagePacketReceived(account, packet);
 483		}
 484	}
 485
 486	private void processPresence(Tag currentTag) throws XmlPullParserException,
 487			IOException {
 488		PresencePacket packet = (PresencePacket) processPacket(currentTag,
 489				PACKET_PRESENCE);
 490		String id = packet.getAttribute("id");
 491		if ((id != null) && (packetCallbacks.containsKey(id))) {
 492			if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) {
 493				((OnPresencePacketReceived) packetCallbacks.get(id))
 494						.onPresencePacketReceived(account, packet);
 495			}
 496			packetCallbacks.remove(id);
 497		} else if (this.presenceListener != null) {
 498			this.presenceListener.onPresencePacketReceived(account, packet);
 499		}
 500	}
 501
 502	private void sendCompressionZlib() throws IOException {
 503		Element compress = new Element("compress");
 504		compress.setAttribute("xmlns", "http://jabber.org/protocol/compress");
 505		compress.addChild("method").setContent("zlib");
 506		tagWriter.writeElement(compress);
 507	}
 508
 509	private void switchOverToZLib(Tag currentTag)
 510			throws XmlPullParserException, IOException,
 511			NoSuchAlgorithmException {
 512		tagReader.readTag(); // read tag close
 513		tagWriter.setOutputStream(new ZLibOutputStream(tagWriter
 514				.getOutputStream()));
 515		tagReader
 516				.setInputStream(new ZLibInputStream(tagReader.getInputStream()));
 517
 518		sendStartStream();
 519		Log.d(Config.LOGTAG, account.getJid() + ": compression enabled");
 520		usingCompression = true;
 521		processStream(tagReader.readTag());
 522	}
 523
 524	private void sendStartTLS() throws IOException {
 525		Tag startTLS = Tag.empty("starttls");
 526		startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
 527		tagWriter.writeTag(startTLS);
 528	}
 529
 530	private SharedPreferences getPreferences() {
 531		return PreferenceManager.getDefaultSharedPreferences(applicationContext);
 532	}
 533
 534	private boolean enableLegacySSL() {
 535		return getPreferences().getBoolean("enable_legacy_ssl", false);
 536	}
 537
 538	private void switchOverToTls(Tag currentTag) throws XmlPullParserException,
 539			IOException {
 540		tagReader.readTag();
 541		try {
 542			SSLContext sc = SSLContext.getInstance("TLS");
 543			sc.init(null,
 544					new X509TrustManager[] { this.mMemorizingTrustManager },
 545					mRandom);
 546			SSLSocketFactory factory = sc.getSocketFactory();
 547
 548			HostnameVerifier verifier = this.mMemorizingTrustManager
 549					.wrapHostnameVerifier(new org.apache.http.conn.ssl.StrictHostnameVerifier());
 550			SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,
 551					socket.getInetAddress().getHostAddress(), socket.getPort(),
 552					true);
 553
 554			// Support all protocols except legacy SSL.
 555			// The min SDK version prevents us having to worry about SSLv2. In future, this may be
 556			// true of SSLv3 as well.
 557			final String[] supportProtocols;
 558			if (enableLegacySSL()) {
 559				supportProtocols = sslSocket.getSupportedProtocols();
 560			} else {
 561				final List<String> supportedProtocols = new LinkedList<String>(Arrays.asList(
 562							sslSocket.getSupportedProtocols()));
 563				supportedProtocols.remove("SSLv3");
 564				supportProtocols = new String[supportedProtocols.size()];
 565				supportedProtocols.toArray(supportProtocols);
 566			}
 567			sslSocket.setEnabledProtocols(supportProtocols);
 568
 569			if (verifier != null
 570					&& !verifier.verify(account.getServer(),
 571							sslSocket.getSession())) {
 572				Log.d(Config.LOGTAG, account.getJid()
 573						+ ": host mismatch in TLS connection");
 574				sslSocket.close();
 575				throw new IOException();
 576			}
 577			tagReader.setInputStream(sslSocket.getInputStream());
 578			tagWriter.setOutputStream(sslSocket.getOutputStream());
 579			sendStartStream();
 580			Log.d(Config.LOGTAG, account.getJid()
 581					+ ": TLS connection established");
 582			usingEncryption = true;
 583			processStream(tagReader.readTag());
 584			sslSocket.close();
 585		} catch (NoSuchAlgorithmException e1) {
 586			e1.printStackTrace();
 587		} catch (KeyManagementException e) {
 588			e.printStackTrace();
 589		}
 590	}
 591
 592	private void sendSaslAuthPlain() throws IOException {
 593		String saslString = CryptoHelper.saslPlain(account.getUsername(),
 594				account.getPassword());
 595		Element auth = new Element("auth");
 596		auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
 597		auth.setAttribute("mechanism", "PLAIN");
 598		auth.setContent(saslString);
 599		tagWriter.writeElement(auth);
 600	}
 601
 602	private void sendSaslAuthDigestMd5() throws IOException {
 603		Element auth = new Element("auth");
 604		auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
 605		auth.setAttribute("mechanism", "DIGEST-MD5");
 606		tagWriter.writeElement(auth);
 607	}
 608
 609	private void processStreamFeatures(Tag currentTag)
 610			throws XmlPullParserException, IOException {
 611		this.streamFeatures = tagReader.readElement(currentTag);
 612		if (this.streamFeatures.hasChild("starttls") && !usingEncryption) {
 613			sendStartTLS();
 614		} else if (compressionAvailable()) {
 615			sendCompressionZlib();
 616		} else if (this.streamFeatures.hasChild("register")
 617				&& account.isOptionSet(Account.OPTION_REGISTER) && usingEncryption) {
 618			sendRegistryRequest();
 619		} else if (!this.streamFeatures.hasChild("register")
 620				&& account.isOptionSet(Account.OPTION_REGISTER)) {
 621			changeStatus(Account.STATUS_REGISTRATION_NOT_SUPPORTED);
 622			disconnect(true);
 623		} else if (this.streamFeatures.hasChild("mechanisms")
 624				&& shouldAuthenticate && usingEncryption) {
 625			List<String> mechanisms = extractMechanisms(streamFeatures
 626					.findChild("mechanisms"));
 627			if (mechanisms.contains("PLAIN")) {
 628				sendSaslAuthPlain();
 629			} else if (mechanisms.contains("DIGEST-MD5")) {
 630				sendSaslAuthDigestMd5();
 631			}
 632		} else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:"
 633				+ smVersion)
 634				&& streamId != null) {
 635			ResumePacket resume = new ResumePacket(this.streamId,
 636					stanzasReceived, smVersion);
 637			this.tagWriter.writeStanzaAsync(resume);
 638		} else if (this.streamFeatures.hasChild("bind") && shouldBind) {
 639			sendBindRequest();
 640		} else {
 641			Log.d(Config.LOGTAG,account.getJid()+": incompatible server. disconnecting");
 642			disconnect(true);
 643		}
 644	}
 645
 646	private boolean compressionAvailable() {
 647		if (!this.streamFeatures.hasChild("compression",
 648				"http://jabber.org/features/compress"))
 649			return false;
 650		if (!ZLibOutputStream.SUPPORTED)
 651			return false;
 652		if (!account.isOptionSet(Account.OPTION_USECOMPRESSION))
 653			return false;
 654
 655		Element compression = this.streamFeatures.findChild("compression",
 656				"http://jabber.org/features/compress");
 657		for (Element child : compression.getChildren()) {
 658			if (!"method".equals(child.getName()))
 659				continue;
 660
 661			if ("zlib".equalsIgnoreCase(child.getContent())) {
 662				return true;
 663			}
 664		}
 665		return false;
 666	}
 667
 668	private List<String> extractMechanisms(Element stream) {
 669		ArrayList<String> mechanisms = new ArrayList<String>(stream
 670				.getChildren().size());
 671		for (Element child : stream.getChildren()) {
 672			mechanisms.add(child.getContent());
 673		}
 674		return mechanisms;
 675	}
 676
 677	private void sendRegistryRequest() {
 678		IqPacket register = new IqPacket(IqPacket.TYPE_GET);
 679		register.query("jabber:iq:register");
 680		register.setTo(account.getServer());
 681		sendIqPacket(register, new OnIqPacketReceived() {
 682
 683			@Override
 684			public void onIqPacketReceived(Account account, IqPacket packet) {
 685				Element instructions = packet.query().findChild("instructions");
 686				if (packet.query().hasChild("username")
 687						&& (packet.query().hasChild("password"))) {
 688					IqPacket register = new IqPacket(IqPacket.TYPE_SET);
 689					Element username = new Element("username")
 690							.setContent(account.getUsername());
 691					Element password = new Element("password")
 692							.setContent(account.getPassword());
 693					register.query("jabber:iq:register").addChild(username);
 694					register.query().addChild(password);
 695					sendIqPacket(register, new OnIqPacketReceived() {
 696
 697						@Override
 698						public void onIqPacketReceived(Account account,
 699								IqPacket packet) {
 700							if (packet.getType() == IqPacket.TYPE_RESULT) {
 701								account.setOption(Account.OPTION_REGISTER,
 702										false);
 703								changeStatus(Account.STATUS_REGISTRATION_SUCCESSFULL);
 704							} else if (packet.hasChild("error")
 705									&& (packet.findChild("error")
 706											.hasChild("conflict"))) {
 707								changeStatus(Account.STATUS_REGISTRATION_CONFLICT);
 708							} else {
 709								changeStatus(Account.STATUS_REGISTRATION_FAILED);
 710								Log.d(Config.LOGTAG, packet.toString());
 711							}
 712							disconnect(true);
 713						}
 714					});
 715				} else {
 716					changeStatus(Account.STATUS_REGISTRATION_FAILED);
 717					disconnect(true);
 718					Log.d(Config.LOGTAG, account.getJid()
 719							+ ": could not register. instructions are"
 720							+ instructions.getContent());
 721				}
 722			}
 723		});
 724	}
 725
 726	private void sendBindRequest() throws IOException {
 727		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
 728		iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind")
 729				.addChild("resource").setContent(account.getResource());
 730		this.sendUnboundIqPacket(iq, new OnIqPacketReceived() {
 731			@Override
 732			public void onIqPacketReceived(Account account, IqPacket packet) {
 733				Element bind = packet.findChild("bind");
 734				if (bind != null) {
 735					Element jid = bind.findChild("jid");
 736					if (jid != null && jid.getContent() != null) {
 737						account.setResource(jid.getContent().split("/", 2)[1]);
 738						if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) {
 739							smVersion = 3;
 740							EnablePacket enable = new EnablePacket(smVersion);
 741							tagWriter.writeStanzaAsync(enable);
 742							stanzasSent = 0;
 743							messageReceipts.clear();
 744						} else if (streamFeatures.hasChild("sm",
 745								"urn:xmpp:sm:2")) {
 746							smVersion = 2;
 747							EnablePacket enable = new EnablePacket(smVersion);
 748							tagWriter.writeStanzaAsync(enable);
 749							stanzasSent = 0;
 750							messageReceipts.clear();
 751						}
 752						sendServiceDiscoveryInfo(account.getServer());
 753						sendServiceDiscoveryItems(account.getServer());
 754						if (bindListener != null) {
 755							bindListener.onBind(account);
 756						}
 757						sendInitialPing();
 758					} else {
 759						disconnect(true);
 760					}
 761				} else {
 762					disconnect(true);
 763				}
 764			}
 765		});
 766		if (this.streamFeatures.hasChild("session")) {
 767			Log.d(Config.LOGTAG, account.getJid()
 768					+ ": sending deprecated session");
 769			IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
 770			startSession.addChild("session",
 771					"urn:ietf:params:xml:ns:xmpp-session");
 772			this.sendUnboundIqPacket(startSession, null);
 773		}
 774	}
 775
 776	private void sendServiceDiscoveryInfo(final String server) {
 777		IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
 778		iq.setTo(server);
 779		iq.query("http://jabber.org/protocol/disco#info");
 780		this.sendIqPacket(iq, new OnIqPacketReceived() {
 781
 782			@Override
 783			public void onIqPacketReceived(Account account, IqPacket packet) {
 784				List<Element> elements = packet.query().getChildren();
 785				List<String> features = new ArrayList<String>();
 786				for (int i = 0; i < elements.size(); ++i) {
 787					if (elements.get(i).getName().equals("feature")) {
 788						features.add(elements.get(i).getAttribute("var"));
 789					}
 790				}
 791				disco.put(server, features);
 792
 793				if (account.getServer().equals(server)) {
 794					enableAdvancedStreamFeatures();
 795				}
 796			}
 797		});
 798	}
 799
 800	private void enableAdvancedStreamFeatures() {
 801		if (getFeatures().carbons()) {
 802			sendEnableCarbons();
 803		}
 804	}
 805
 806	private void sendServiceDiscoveryItems(final String server) {
 807		IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
 808		iq.setTo(server);
 809		iq.query("http://jabber.org/protocol/disco#items");
 810		this.sendIqPacket(iq, new OnIqPacketReceived() {
 811
 812			@Override
 813			public void onIqPacketReceived(Account account, IqPacket packet) {
 814				List<Element> elements = packet.query().getChildren();
 815				for (int i = 0; i < elements.size(); ++i) {
 816					if (elements.get(i).getName().equals("item")) {
 817						String jid = elements.get(i).getAttribute("jid");
 818						sendServiceDiscoveryInfo(jid);
 819					}
 820				}
 821			}
 822		});
 823	}
 824
 825	private void sendEnableCarbons() {
 826		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
 827		iq.addChild("enable", "urn:xmpp:carbons:2");
 828		this.sendIqPacket(iq, new OnIqPacketReceived() {
 829
 830			@Override
 831			public void onIqPacketReceived(Account account, IqPacket packet) {
 832				if (!packet.hasChild("error")) {
 833					Log.d(Config.LOGTAG, account.getJid()
 834							+ ": successfully enabled carbons");
 835				} else {
 836					Log.d(Config.LOGTAG, account.getJid()
 837							+ ": error enableing carbons " + packet.toString());
 838				}
 839			}
 840		});
 841	}
 842
 843	private void processStreamError(Tag currentTag)
 844			throws XmlPullParserException, IOException {
 845		Element streamError = tagReader.readElement(currentTag);
 846		if (streamError != null && streamError.hasChild("conflict")) {
 847			String resource = account.getResource().split("\\.")[0];
 848			account.setResource(resource + "." + nextRandomId());
 849			Log.d(Config.LOGTAG,
 850					account.getJid() + ": switching resource due to conflict ("
 851							+ account.getResource() + ")");
 852		}
 853	}
 854
 855	private void sendStartStream() throws IOException {
 856		Tag stream = Tag.start("stream:stream");
 857		stream.setAttribute("from", account.getJid());
 858		stream.setAttribute("to", account.getServer());
 859		stream.setAttribute("version", "1.0");
 860		stream.setAttribute("xml:lang", "en");
 861		stream.setAttribute("xmlns", "jabber:client");
 862		stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
 863		tagWriter.writeTag(stream);
 864	}
 865
 866	private String nextRandomId() {
 867		return new BigInteger(50, mRandom).toString(32);
 868	}
 869
 870	public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
 871		if (packet.getId() == null) {
 872			String id = nextRandomId();
 873			packet.setAttribute("id", id);
 874		}
 875		packet.setFrom(account.getFullJid());
 876		this.sendPacket(packet, callback);
 877	}
 878
 879	public void sendUnboundIqPacket(IqPacket packet, OnIqPacketReceived callback) {
 880		if (packet.getId() == null) {
 881			String id = nextRandomId();
 882			packet.setAttribute("id", id);
 883		}
 884		this.sendPacket(packet, callback);
 885	}
 886
 887	public void sendMessagePacket(MessagePacket packet) {
 888		this.sendPacket(packet, null);
 889	}
 890
 891	public void sendPresencePacket(PresencePacket packet) {
 892		this.sendPacket(packet, null);
 893	}
 894
 895	private synchronized void sendPacket(final AbstractStanza packet,
 896			PacketReceived callback) {
 897		if (packet.getName().equals("iq") || packet.getName().equals("message")
 898				|| packet.getName().equals("presence")) {
 899			++stanzasSent;
 900		}
 901		tagWriter.writeStanzaAsync(packet);
 902		if (packet instanceof MessagePacket && packet.getId() != null
 903				&& this.streamId != null) {
 904			Log.d(Config.LOGTAG, "request delivery report for stanza "
 905					+ stanzasSent);
 906			this.messageReceipts.put(stanzasSent, packet.getId());
 907			tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion));
 908		}
 909		if (callback != null) {
 910			if (packet.getId() == null) {
 911				packet.setId(nextRandomId());
 912			}
 913			packetCallbacks.put(packet.getId(), callback);
 914		}
 915	}
 916
 917	public void sendPing() {
 918		if (streamFeatures.hasChild("sm")) {
 919			tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
 920		} else {
 921			IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
 922			iq.setFrom(account.getFullJid());
 923			iq.addChild("ping", "urn:xmpp:ping");
 924			this.sendIqPacket(iq, null);
 925		}
 926		this.lastPingSent = SystemClock.elapsedRealtime();
 927	}
 928
 929	public void setOnMessagePacketReceivedListener(
 930			OnMessagePacketReceived listener) {
 931		this.messageListener = listener;
 932	}
 933
 934	public void setOnUnregisteredIqPacketReceivedListener(
 935			OnIqPacketReceived listener) {
 936		this.unregisteredIqListener = listener;
 937	}
 938
 939	public void setOnPresencePacketReceivedListener(
 940			OnPresencePacketReceived listener) {
 941		this.presenceListener = listener;
 942	}
 943
 944	public void setOnJinglePacketReceivedListener(
 945			OnJinglePacketReceived listener) {
 946		this.jingleListener = listener;
 947	}
 948
 949	public void setOnStatusChangedListener(OnStatusChanged listener) {
 950		this.statusListener = listener;
 951	}
 952
 953	public void setOnBindListener(OnBindListener listener) {
 954		this.bindListener = listener;
 955	}
 956
 957	public void setOnMessageAcknowledgeListener(OnMessageAcknowledged listener) {
 958		this.acknowledgedListener = listener;
 959	}
 960
 961	public void disconnect(boolean force) {
 962		Log.d(Config.LOGTAG, account.getJid() + ": disconnecting");
 963		try {
 964			if (force) {
 965				socket.close();
 966				return;
 967			}
 968			new Thread(new Runnable() {
 969
 970				@Override
 971				public void run() {
 972					if (tagWriter.isActive()) {
 973						tagWriter.finish();
 974						try {
 975							while (!tagWriter.finished()) {
 976								Log.d(Config.LOGTAG, "not yet finished");
 977								Thread.sleep(100);
 978							}
 979							tagWriter.writeTag(Tag.end("stream:stream"));
 980							socket.close();
 981						} catch (IOException e) {
 982							Log.d(Config.LOGTAG,
 983									"io exception during disconnect");
 984						} catch (InterruptedException e) {
 985							Log.d(Config.LOGTAG, "interrupted");
 986						}
 987					}
 988				}
 989			}).start();
 990		} catch (IOException e) {
 991			Log.d(Config.LOGTAG, "io exception during disconnect");
 992		}
 993	}
 994
 995	public List<String> findDiscoItemsByFeature(String feature) {
 996		List<String> items = new ArrayList<String>();
 997		for (Entry<String, List<String>> cursor : disco.entrySet()) {
 998			if (cursor.getValue().contains(feature)) {
 999				items.add(cursor.getKey());
1000			}
1001		}
1002		return items;
1003	}
1004
1005	public String findDiscoItemByFeature(String feature) {
1006		List<String> items = findDiscoItemsByFeature(feature);
1007		if (items.size() >= 1) {
1008			return items.get(0);
1009		}
1010		return null;
1011	}
1012
1013	public void r() {
1014		this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
1015	}
1016
1017	public String getMucServer() {
1018		return findDiscoItemByFeature("http://jabber.org/protocol/muc");
1019	}
1020
1021	public int getTimeToNextAttempt() {
1022		int interval = (int) (25 * Math.pow(1.5, attempt));
1023		int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
1024		return interval - secondsSinceLast;
1025	}
1026
1027	public int getAttempt() {
1028		return this.attempt;
1029	}
1030
1031	public Features getFeatures() {
1032		return this.features;
1033	}
1034
1035	public class Features {
1036		XmppConnection connection;
1037
1038		public Features(XmppConnection connection) {
1039			this.connection = connection;
1040		}
1041
1042		private boolean hasDiscoFeature(String server, String feature) {
1043			if (!connection.disco.containsKey(server)) {
1044				return false;
1045			}
1046			return connection.disco.get(server).contains(feature);
1047		}
1048
1049		public boolean carbons() {
1050			return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2");
1051		}
1052
1053		public boolean sm() {
1054			return streamId != null;
1055		}
1056
1057		public boolean csi() {
1058			if (connection.streamFeatures == null) {
1059				return false;
1060			} else {
1061				return connection.streamFeatures.hasChild("csi",
1062						"urn:xmpp:csi:0");
1063			}
1064		}
1065
1066		public boolean pubsub() {
1067			return hasDiscoFeature(account.getServer(),
1068					"http://jabber.org/protocol/pubsub#publish");
1069		}
1070
1071		public boolean rosterVersioning() {
1072			if (connection.streamFeatures == null) {
1073				return false;
1074			} else {
1075				return connection.streamFeatures.hasChild("ver");
1076			}
1077		}
1078
1079		public boolean streamhost() {
1080			return connection
1081					.findDiscoItemByFeature("http://jabber.org/protocol/bytestreams") != null;
1082		}
1083
1084		public boolean compression() {
1085			return connection.usingCompression;
1086		}
1087	}
1088
1089	public long getLastSessionEstablished() {
1090		long diff;
1091		if (this.lastSessionStarted == 0) {
1092			diff = SystemClock.elapsedRealtime() - this.lastConnect;
1093		} else {
1094			diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
1095		}
1096		return System.currentTimeMillis() - diff;
1097	}
1098
1099	public long getLastConnect() {
1100		return this.lastConnect;
1101	}
1102
1103	public long getLastPingSent() {
1104		return this.lastPingSent;
1105	}
1106
1107	public long getLastPacketReceived() {
1108		return this.lastPaketReceived;
1109	}
1110
1111	public void sendActive() {
1112		this.sendPacket(new ActivePacket(), null);
1113	}
1114
1115	public void sendInactive() {
1116		this.sendPacket(new InactivePacket(), null);
1117	}
1118}