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