XmppConnection.java

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