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								Log.d(Config.LOGTAG,account.getJid().toBareJid()+": TLS certificate verification failed");
 603								disconnect(true);
 604								changeStatus(Account.State.SECURITY_ERROR);
 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					changeStatus(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,account.getJid().toString()+": 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			disconnect(true);
 677			changeStatus(Account.State.INCOMPATIBLE_SERVER);
 678		}
 679	}
 680
 681	private boolean compressionAvailable() {
 682		if (!this.streamFeatures.hasChild("compression",
 683					"http://jabber.org/features/compress"))
 684			return false;
 685		if (!ZLibOutputStream.SUPPORTED)
 686			return false;
 687		if (!account.isOptionSet(Account.OPTION_USECOMPRESSION))
 688			return false;
 689
 690		Element compression = this.streamFeatures.findChild("compression",
 691				"http://jabber.org/features/compress");
 692		for (Element child : compression.getChildren()) {
 693			if (!"method".equals(child.getName()))
 694				continue;
 695
 696			if ("zlib".equalsIgnoreCase(child.getContent())) {
 697				return true;
 698			}
 699		}
 700		return false;
 701	}
 702
 703	private List<String> extractMechanisms(Element stream) {
 704		ArrayList<String> mechanisms = new ArrayList<>(stream
 705				.getChildren().size());
 706		for (Element child : stream.getChildren()) {
 707			mechanisms.add(child.getContent());
 708		}
 709		return mechanisms;
 710	}
 711
 712	private void sendRegistryRequest() {
 713		IqPacket register = new IqPacket(IqPacket.TYPE_GET);
 714		register.query("jabber:iq:register");
 715		register.setTo(account.getServer());
 716		sendIqPacket(register, new OnIqPacketReceived() {
 717
 718			@Override
 719			public void onIqPacketReceived(Account account, IqPacket packet) {
 720				Element instructions = packet.query().findChild("instructions");
 721				if (packet.query().hasChild("username")
 722						&& (packet.query().hasChild("password"))) {
 723					IqPacket register = new IqPacket(IqPacket.TYPE_SET);
 724					Element username = new Element("username")
 725						.setContent(account.getUsername());
 726					Element password = new Element("password")
 727						.setContent(account.getPassword());
 728					register.query("jabber:iq:register").addChild(username);
 729					register.query().addChild(password);
 730					sendIqPacket(register, new OnIqPacketReceived() {
 731
 732						@Override
 733						public void onIqPacketReceived(Account account,
 734								IqPacket packet) {
 735							if (packet.getType() == IqPacket.TYPE_RESULT) {
 736								account.setOption(Account.OPTION_REGISTER,
 737										false);
 738								changeStatus(Account.State.REGISTRATION_SUCCESSFUL);
 739							} else if (packet.hasChild("error")
 740									&& (packet.findChild("error")
 741										.hasChild("conflict"))) {
 742								changeStatus(Account.State.REGISTRATION_CONFLICT);
 743							} else {
 744								changeStatus(Account.State.REGISTRATION_FAILED);
 745								Log.d(Config.LOGTAG, packet.toString());
 746							}
 747							disconnect(true);
 748						}
 749					});
 750				} else {
 751					changeStatus(Account.State.REGISTRATION_FAILED);
 752					disconnect(true);
 753					Log.d(Config.LOGTAG, account.getJid().toBareJid()
 754							+ ": could not register. instructions are"
 755							+ instructions.getContent());
 756				}
 757			}
 758		});
 759	}
 760
 761	private void sendBindRequest() throws IOException {
 762		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
 763		iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind")
 764			.addChild("resource").setContent(account.getResource());
 765		this.sendUnboundIqPacket(iq, new OnIqPacketReceived() {
 766			@Override
 767			public void onIqPacketReceived(Account account, IqPacket packet) {
 768				Element bind = packet.findChild("bind");
 769				if (bind != null) {
 770					final Element jid = bind.findChild("jid");
 771					if (jid != null && jid.getContent() != null) {
 772						try {
 773							account.setResource(Jid.fromString(jid.getContent()).getResourcepart());
 774						} catch (final InvalidJidException e) {
 775							// TODO: Handle the case where an external JID is technically invalid?
 776						}
 777						if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) {
 778							smVersion = 3;
 779							EnablePacket enable = new EnablePacket(smVersion);
 780							tagWriter.writeStanzaAsync(enable);
 781							stanzasSent = 0;
 782							messageReceipts.clear();
 783						} else if (streamFeatures.hasChild("sm",
 784									"urn:xmpp:sm:2")) {
 785							smVersion = 2;
 786							EnablePacket enable = new EnablePacket(smVersion);
 787							tagWriter.writeStanzaAsync(enable);
 788							stanzasSent = 0;
 789							messageReceipts.clear();
 790						}
 791						enabledCarbons = false;
 792						disco.clear();
 793						sendServiceDiscoveryInfo(account.getServer());
 794						sendServiceDiscoveryItems(account.getServer());
 795						if (bindListener != null) {
 796							bindListener.onBind(account);
 797						}
 798						sendInitialPing();
 799					} else {
 800						disconnect(true);
 801					}
 802				} else {
 803					disconnect(true);
 804				}
 805			}
 806		});
 807		if (this.streamFeatures.hasChild("session")) {
 808			Log.d(Config.LOGTAG, account.getJid().toBareJid()
 809					+ ": sending deprecated session");
 810			IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
 811			startSession.addChild("session",
 812					"urn:ietf:params:xml:ns:xmpp-session");
 813			this.sendUnboundIqPacket(startSession, null);
 814		}
 815	}
 816
 817	private void sendServiceDiscoveryInfo(final Jid server) {
 818		if (disco.containsKey(server.toDomainJid().toString())) {
 819			if (account.getServer().equals(server.toDomainJid())) {
 820				enableAdvancedStreamFeatures();
 821			}
 822		} else {
 823			final IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
 824			iq.setTo(server.toDomainJid());
 825			iq.query("http://jabber.org/protocol/disco#info");
 826			this.sendIqPacket(iq, new OnIqPacketReceived() {
 827
 828				@Override
 829				public void onIqPacketReceived(Account account, IqPacket packet) {
 830					final List<Element> elements = packet.query().getChildren();
 831					final List<String> features = new ArrayList<>();
 832					for (Element element : elements) {
 833						if (element.getName().equals("feature")) {
 834							features.add(element.getAttribute("var"));
 835						}
 836					}
 837					disco.put(server.toDomainJid().toString(), features);
 838
 839					if (account.getServer().equals(server.toDomainJid())) {
 840						enableAdvancedStreamFeatures();
 841					}
 842				}
 843			});
 844		}
 845	}
 846
 847	private void enableAdvancedStreamFeatures() {
 848		if (getFeatures().carbons()) {
 849			if (!enabledCarbons) {
 850				sendEnableCarbons();
 851			}
 852		}
 853	}
 854
 855	private void sendServiceDiscoveryItems(final Jid server) {
 856		final IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
 857		iq.setTo(server.toDomainJid());
 858		iq.query("http://jabber.org/protocol/disco#items");
 859		this.sendIqPacket(iq, new OnIqPacketReceived() {
 860
 861			@Override
 862			public void onIqPacketReceived(Account account, IqPacket packet) {
 863				List<Element> elements = packet.query().getChildren();
 864				for (Element element : elements) {
 865					if (element.getName().equals("item")) {
 866						final String jid = element.getAttribute("jid");
 867						try {
 868							sendServiceDiscoveryInfo(Jid.fromString(jid).toDomainJid());
 869						} catch (final InvalidJidException ignored) {
 870							// TODO: Handle the case where an external JID is technically invalid?
 871						}
 872					}
 873				}
 874			}
 875		});
 876	}
 877
 878	private void sendEnableCarbons() {
 879		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
 880		iq.addChild("enable", "urn:xmpp:carbons:2");
 881		this.sendIqPacket(iq, new OnIqPacketReceived() {
 882
 883			@Override
 884			public void onIqPacketReceived(Account account, IqPacket packet) {
 885				if (!packet.hasChild("error")) {
 886					Log.d(Config.LOGTAG, account.getJid().toBareJid()
 887							+ ": successfully enabled carbons");
 888					enabledCarbons = true;
 889				} else {
 890					Log.d(Config.LOGTAG, account.getJid().toBareJid()
 891							+ ": error enableing carbons " + packet.toString());
 892				}
 893			}
 894		});
 895	}
 896
 897	private void processStreamError(Tag currentTag)
 898		throws XmlPullParserException, IOException {
 899		Element streamError = tagReader.readElement(currentTag);
 900		if (streamError != null && streamError.hasChild("conflict")) {
 901			final String resource = account.getResource().split("\\.")[0];
 902			account.setResource(resource + "." + nextRandomId());
 903			Log.d(Config.LOGTAG,
 904					account.getJid().toBareJid() + ": switching resource due to conflict ("
 905					+ account.getResource() + ")");
 906		}
 907	}
 908
 909	private void sendStartStream() throws IOException {
 910		Tag stream = Tag.start("stream:stream");
 911		stream.setAttribute("from", account.getJid().toBareJid().toString());
 912		stream.setAttribute("to", account.getServer().toString());
 913		stream.setAttribute("version", "1.0");
 914		stream.setAttribute("xml:lang", "en");
 915		stream.setAttribute("xmlns", "jabber:client");
 916		stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
 917		tagWriter.writeTag(stream);
 918	}
 919
 920	private String nextRandomId() {
 921		return new BigInteger(50, mXmppConnectionService.getRNG()).toString(32);
 922	}
 923
 924	public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
 925		if (packet.getId() == null) {
 926			String id = nextRandomId();
 927			packet.setAttribute("id", id);
 928		}
 929		packet.setFrom(account.getJid());
 930		this.sendPacket(packet, callback);
 931	}
 932
 933	public void sendUnboundIqPacket(IqPacket packet, OnIqPacketReceived callback) {
 934		if (packet.getId() == null) {
 935			String id = nextRandomId();
 936			packet.setAttribute("id", id);
 937		}
 938		this.sendPacket(packet, callback);
 939	}
 940
 941	public void sendMessagePacket(MessagePacket packet) {
 942		this.sendPacket(packet, null);
 943	}
 944
 945	public void sendPresencePacket(PresencePacket packet) {
 946		this.sendPacket(packet, null);
 947	}
 948
 949	private synchronized void sendPacket(final AbstractStanza packet,
 950			PacketReceived callback) {
 951		if (packet.getName().equals("iq") || packet.getName().equals("message")
 952				|| packet.getName().equals("presence")) {
 953			++stanzasSent;
 954				}
 955		tagWriter.writeStanzaAsync(packet);
 956		if (packet instanceof MessagePacket && packet.getId() != null
 957				&& this.streamId != null) {
 958			Log.d(Config.LOGTAG, "request delivery report for stanza "
 959					+ stanzasSent);
 960			this.messageReceipts.put(stanzasSent, packet.getId());
 961			tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion));
 962				}
 963		if (callback != null) {
 964			if (packet.getId() == null) {
 965				packet.setId(nextRandomId());
 966			}
 967			packetCallbacks.put(packet.getId(), callback);
 968		}
 969	}
 970
 971	public void sendPing() {
 972		if (streamFeatures.hasChild("sm")) {
 973			tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
 974		} else {
 975			IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
 976			iq.setFrom(account.getJid());
 977			iq.addChild("ping", "urn:xmpp:ping");
 978			this.sendIqPacket(iq, null);
 979		}
 980		this.lastPingSent = SystemClock.elapsedRealtime();
 981	}
 982
 983	public void setOnMessagePacketReceivedListener(
 984			OnMessagePacketReceived listener) {
 985		this.messageListener = listener;
 986			}
 987
 988	public void setOnUnregisteredIqPacketReceivedListener(
 989			OnIqPacketReceived listener) {
 990		this.unregisteredIqListener = listener;
 991			}
 992
 993	public void setOnPresencePacketReceivedListener(
 994			OnPresencePacketReceived listener) {
 995		this.presenceListener = listener;
 996			}
 997
 998	public void setOnJinglePacketReceivedListener(
 999			OnJinglePacketReceived listener) {
1000		this.jingleListener = listener;
1001			}
1002
1003	public void setOnStatusChangedListener(OnStatusChanged listener) {
1004		this.statusListener = listener;
1005	}
1006
1007	public void setOnBindListener(OnBindListener listener) {
1008		this.bindListener = listener;
1009	}
1010
1011	public void setOnMessageAcknowledgeListener(OnMessageAcknowledged listener) {
1012		this.acknowledgedListener = listener;
1013	}
1014
1015	public void disconnect(boolean force) {
1016		Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": disconnecting");
1017		try {
1018			if (force) {
1019				socket.close();
1020				return;
1021			}
1022			new Thread(new Runnable() {
1023
1024				@Override
1025				public void run() {
1026					if (tagWriter.isActive()) {
1027						tagWriter.finish();
1028						try {
1029							while (!tagWriter.finished()) {
1030								Log.d(Config.LOGTAG, "not yet finished");
1031								Thread.sleep(100);
1032							}
1033							tagWriter.writeTag(Tag.end("stream:stream"));
1034							socket.close();
1035						} catch (IOException e) {
1036							Log.d(Config.LOGTAG,
1037									"io exception during disconnect");
1038						} catch (InterruptedException e) {
1039							Log.d(Config.LOGTAG, "interrupted");
1040						}
1041					}
1042				}
1043			}).start();
1044		} catch (IOException e) {
1045			Log.d(Config.LOGTAG, "io exception during disconnect");
1046		}
1047	}
1048
1049	public List<String> findDiscoItemsByFeature(String feature) {
1050		final List<String> items = new ArrayList<>();
1051		for (Entry<String, List<String>> cursor : disco.entrySet()) {
1052			if (cursor.getValue().contains(feature)) {
1053				items.add(cursor.getKey());
1054			}
1055		}
1056		return items;
1057	}
1058
1059	public String findDiscoItemByFeature(String feature) {
1060		List<String> items = findDiscoItemsByFeature(feature);
1061		if (items.size() >= 1) {
1062			return items.get(0);
1063		}
1064		return null;
1065	}
1066
1067	public void r() {
1068		this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
1069	}
1070
1071	public String getMucServer() {
1072		return findDiscoItemByFeature("http://jabber.org/protocol/muc");
1073	}
1074
1075	public int getTimeToNextAttempt() {
1076		int interval = (int) (25 * Math.pow(1.5, attempt));
1077		int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
1078		return interval - secondsSinceLast;
1079	}
1080
1081	public int getAttempt() {
1082		return this.attempt;
1083	}
1084
1085	public Features getFeatures() {
1086		return this.features;
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
1119	public class Features {
1120		XmppConnection connection;
1121
1122		public Features(XmppConnection connection) {
1123			this.connection = connection;
1124		}
1125
1126		private boolean hasDiscoFeature(final Jid server, final String feature) {
1127			return connection.disco.containsKey(server.toDomainJid().toString()) &&
1128				connection.disco.get(server.toDomainJid().toString()).contains(feature);
1129		}
1130
1131		public boolean carbons() {
1132			return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2");
1133		}
1134
1135		public boolean sm() {
1136			return streamId != null;
1137		}
1138
1139		public boolean csi() {
1140			return connection.streamFeatures != null && connection.streamFeatures.hasChild("csi", "urn:xmpp:csi:0");
1141		}
1142
1143		public boolean pubsub() {
1144			return hasDiscoFeature(account.getServer(),
1145					"http://jabber.org/protocol/pubsub#publish");
1146		}
1147
1148		public boolean mam() {
1149			return hasDiscoFeature(account.getServer(), "urn:xmpp:mam:0");
1150		}
1151
1152		public boolean rosterVersioning() {
1153			return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver");
1154		}
1155
1156		public boolean streamhost() {
1157			return connection
1158				.findDiscoItemByFeature("http://jabber.org/protocol/bytestreams") != null;
1159		}
1160
1161		public boolean compression() {
1162			return connection.enabledCompression;
1163		}
1164	}
1165}