XmppConnection.java

  1package eu.siacs.conversations.xmpp;
  2
  3import java.io.IOException;
  4import java.io.InputStream;
  5import java.io.OutputStream;
  6import java.math.BigInteger;
  7import java.net.Socket;
  8import java.net.UnknownHostException;
  9import java.security.SecureRandom;
 10import java.util.HashSet;
 11import java.util.Hashtable;
 12import java.util.List;
 13
 14import javax.net.ssl.SSLSocket;
 15import javax.net.ssl.SSLSocketFactory;
 16
 17import org.xmlpull.v1.XmlPullParserException;
 18
 19import android.os.Bundle;
 20import android.os.PowerManager;
 21import android.util.Log;
 22import eu.siacs.conversations.entities.Account;
 23import eu.siacs.conversations.utils.DNSHelper;
 24import eu.siacs.conversations.utils.SASL;
 25import eu.siacs.conversations.xml.Element;
 26import eu.siacs.conversations.xml.Tag;
 27import eu.siacs.conversations.xml.TagWriter;
 28import eu.siacs.conversations.xml.XmlReader;
 29
 30public class XmppConnection implements Runnable {
 31
 32	protected Account account;
 33	private static final String LOGTAG = "xmppService";
 34
 35	private PowerManager.WakeLock wakeLock;
 36
 37	private SecureRandom random = new SecureRandom();
 38
 39	private Socket socket;
 40	private XmlReader tagReader;
 41	private TagWriter tagWriter;
 42
 43	private boolean isTlsEncrypted = false;
 44	private boolean isAuthenticated = false;
 45	// private boolean shouldUseTLS = false;
 46	private boolean shouldConnect = true;
 47	private boolean shouldBind = true;
 48	private boolean shouldAuthenticate = true;
 49	private Element streamFeatures;
 50	private HashSet<String> discoFeatures = new HashSet<String>();
 51
 52	private static final int PACKET_IQ = 0;
 53	private static final int PACKET_MESSAGE = 1;
 54	private static final int PACKET_PRESENCE = 2;
 55
 56	private Hashtable<String, PacketReceived> packetCallbacks = new Hashtable<String, PacketReceived>();
 57	private OnPresencePacketReceived presenceListener = null;
 58	private OnIqPacketReceived unregisteredIqListener = null;
 59	private OnMessagePacketReceived messageListener = null;
 60	private OnStatusChanged statusListener = null;
 61
 62	public XmppConnection(Account account, PowerManager pm) {
 63		this.account = account;
 64		wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
 65				"XmppConnection");
 66		tagReader = new XmlReader(wakeLock);
 67		tagWriter = new TagWriter();
 68	}
 69	
 70	protected void changeStatus(int nextStatus) {
 71		account.setStatus(nextStatus);
 72		if (statusListener != null) {
 73			statusListener.onStatusChanged(account);
 74		}
 75	}
 76
 77	protected void connect() {
 78		try {
 79			this.changeStatus(Account.STATUS_CONNECTING);
 80			Bundle namePort = DNSHelper.getSRVRecord(account.getServer());
 81			String srvRecordServer = namePort.getString("name");
 82			int srvRecordPort = namePort.getInt("port");
 83			if (srvRecordServer != null) {
 84				Log.d(LOGTAG, account.getJid() + ": using values from dns "
 85						+ srvRecordServer + ":" + srvRecordPort);
 86				socket = new Socket(srvRecordServer, srvRecordPort);
 87			} else {
 88				socket = new Socket(account.getServer(), 5222);
 89			}
 90			OutputStream out = socket.getOutputStream();
 91			tagWriter.setOutputStream(out);
 92			InputStream in = socket.getInputStream();
 93			tagReader.setInputStream(in);
 94			tagWriter.beginDocument();
 95			sendStartStream();
 96			Tag nextTag;
 97			while ((nextTag = tagReader.readTag()) != null) {
 98				if (nextTag.isStart("stream")) {
 99					processStream(nextTag);
100					break;
101				} else {
102					Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName());
103					return;
104				}
105			}
106			if (socket.isConnected()) {
107				socket.close();
108			}
109		} catch (UnknownHostException e) {
110			this.changeStatus(Account.STATUS_SERVER_NOT_FOUND);
111			if (wakeLock.isHeld()) {
112				wakeLock.release();
113			}
114			return;
115		} catch (IOException e) {
116			this.changeStatus(Account.STATUS_OFFLINE);
117			if (wakeLock.isHeld()) {
118				wakeLock.release();
119			}
120			return;
121		} catch (XmlPullParserException e) {
122			this.changeStatus(Account.STATUS_OFFLINE);
123			Log.d(LOGTAG, "xml exception " + e.getMessage());
124			if (wakeLock.isHeld()) {
125				wakeLock.release();
126			}
127			return;
128		}
129
130	}
131
132	@Override
133	public void run() {
134		connect();
135		Log.d(LOGTAG, "end run");
136	}
137
138	private void processStream(Tag currentTag) throws XmlPullParserException,
139			IOException {
140		Tag nextTag = tagReader.readTag();
141		while ((nextTag != null) && (!nextTag.isEnd("stream"))) {
142			if (nextTag.isStart("error")) {
143				processStreamError(nextTag);
144			} else if (nextTag.isStart("features")) {
145				processStreamFeatures(nextTag);
146			} else if (nextTag.isStart("proceed")) {
147				switchOverToTls(nextTag);
148			} else if (nextTag.isStart("success")) {
149				isAuthenticated = true;
150				Log.d(LOGTAG, account.getJid()
151						+ ": read success tag in stream. reset again");
152				tagReader.readTag();
153				tagReader.reset();
154				sendStartStream();
155				processStream(tagReader.readTag());
156				break;
157			} else if (nextTag.isStart("failure")) {
158				Element failure = tagReader.readElement(nextTag);
159				Log.d(LOGTAG, "read failure element" + failure.toString());
160				account.setStatus(Account.STATUS_UNAUTHORIZED);
161				if (statusListener != null) {
162					statusListener.onStatusChanged(account);
163				}
164				tagWriter.writeTag(Tag.end("stream"));
165			} else if (nextTag.isStart("iq")) {
166				processIq(nextTag);
167			} else if (nextTag.isStart("message")) {
168				processMessage(nextTag);
169			} else if (nextTag.isStart("presence")) {
170				processPresence(nextTag);
171			} else {
172				Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName()
173						+ " as child of " + currentTag.getName());
174			}
175			nextTag = tagReader.readTag();
176		}
177		if (account.getStatus() == Account.STATUS_ONLINE) {
178			account.setStatus(Account.STATUS_OFFLINE);
179			if (statusListener != null) {
180				statusListener.onStatusChanged(account);
181			}
182		}
183	}
184
185	private Element processPacket(Tag currentTag, int packetType)
186			throws XmlPullParserException, IOException {
187		Element element;
188		switch (packetType) {
189		case PACKET_IQ:
190			element = new IqPacket();
191			break;
192		case PACKET_MESSAGE:
193			element = new MessagePacket();
194			break;
195		case PACKET_PRESENCE:
196			element = new PresencePacket();
197			break;
198		default:
199			return null;
200		}
201		element.setAttributes(currentTag.getAttributes());
202		Tag nextTag = tagReader.readTag();
203		while (!nextTag.isEnd(element.getName())) {
204			if (!nextTag.isNo()) {
205				Element child = tagReader.readElement(nextTag);
206				element.addChild(child);
207			}
208			nextTag = tagReader.readTag();
209		}
210		return element;
211	}
212
213	private void processIq(Tag currentTag) throws XmlPullParserException,
214			IOException {
215		IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
216		if (packetCallbacks.containsKey(packet.getId())) {
217			if (packetCallbacks.get(packet.getId()) instanceof OnIqPacketReceived) {
218				((OnIqPacketReceived) packetCallbacks.get(packet.getId())).onIqPacketReceived(account,
219						packet);
220			}
221			
222			packetCallbacks.remove(packet.getId());
223		} else if (this.unregisteredIqListener != null) {
224			this.unregisteredIqListener.onIqPacketReceived(account, packet);
225		}
226	}
227
228	private void processMessage(Tag currentTag) throws XmlPullParserException,
229			IOException {
230		MessagePacket packet = (MessagePacket) processPacket(currentTag,
231				PACKET_MESSAGE);
232		String id = packet.getAttribute("id");
233		if ((id!=null)&&(packetCallbacks.containsKey(id))) {
234			if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) {
235				((OnMessagePacketReceived) packetCallbacks.get(id)).onMessagePacketReceived(account,
236						packet);
237			}
238			packetCallbacks.remove(id);
239		} else if (this.messageListener != null) {
240			this.messageListener.onMessagePacketReceived(account, packet);
241		}
242	}
243
244	private void processPresence(Tag currentTag) throws XmlPullParserException,
245			IOException {
246		PresencePacket packet = (PresencePacket) processPacket(currentTag,
247				PACKET_PRESENCE);
248		String id = packet.getAttribute("id");
249		if ((id!=null)&&(packetCallbacks.containsKey(id))) {
250			if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) {
251				((OnPresencePacketReceived) packetCallbacks.get(id)).onPresencePacketReceived(account,
252						packet);
253			}
254			packetCallbacks.remove(id);
255		} else if (this.presenceListener != null) {
256			this.presenceListener.onPresencePacketReceived(account, packet);
257		}
258	}
259
260	private void sendStartTLS() {
261		Tag startTLS = Tag.empty("starttls");
262		startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
263		Log.d(LOGTAG, account.getJid() + ": sending starttls");
264		tagWriter.writeTag(startTLS);
265	}
266
267	private void switchOverToTls(Tag currentTag) throws XmlPullParserException,
268			IOException {
269		Tag nextTag = tagReader.readTag(); // should be proceed end tag
270		Log.d(LOGTAG, account.getJid() + ": now switch to ssl");
271		SSLSocket sslSocket;
272		try {
273			sslSocket = (SSLSocket) ((SSLSocketFactory) SSLSocketFactory
274					.getDefault()).createSocket(socket, socket.getInetAddress()
275					.getHostAddress(), socket.getPort(), true);
276			tagReader.setInputStream(sslSocket.getInputStream());
277			Log.d(LOGTAG, "reset inputstream");
278			tagWriter.setOutputStream(sslSocket.getOutputStream());
279			Log.d(LOGTAG, "switch over seemed to work");
280			isTlsEncrypted = true;
281			sendStartStream();
282			processStream(tagReader.readTag());
283			sslSocket.close();
284		} catch (IOException e) {
285			Log.d(LOGTAG,
286					account.getJid() + ": error on ssl '" + e.getMessage()
287							+ "'");
288		}
289	}
290
291	private void sendSaslAuth() throws IOException, XmlPullParserException {
292		String saslString = SASL.plain(account.getUsername(),
293				account.getPassword());
294		Element auth = new Element("auth");
295		auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
296		auth.setAttribute("mechanism", "PLAIN");
297		auth.setContent(saslString);
298		Log.d(LOGTAG, account.getJid() + ": sending sasl " + auth.toString());
299		tagWriter.writeElement(auth);
300	}
301
302	private void processStreamFeatures(Tag currentTag)
303			throws XmlPullParserException, IOException {
304		this.streamFeatures = tagReader.readElement(currentTag);
305		Log.d(LOGTAG, account.getJid() + ": process stream features "
306				+ streamFeatures);
307		if (this.streamFeatures.hasChild("starttls")
308				&& account.isOptionSet(Account.OPTION_USETLS)) {
309			sendStartTLS();
310		} else if (this.streamFeatures.hasChild("mechanisms")
311				&& shouldAuthenticate) {
312			sendSaslAuth();
313		}
314		if (this.streamFeatures.hasChild("bind") && shouldBind) {
315			sendBindRequest();
316			if (this.streamFeatures.hasChild("session")) {
317				IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
318				Element session = new Element("session");
319				session.setAttribute("xmlns",
320						"urn:ietf:params:xml:ns:xmpp-session");
321				session.setContent("");
322				startSession.addChild(session);
323				sendIqPacket(startSession, null);
324				tagWriter.writeElement(startSession);
325			}
326			Element presence = new Element("presence");
327
328			tagWriter.writeElement(presence);
329		}
330	}
331
332	private void sendBindRequest() throws IOException {
333		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
334		Element bind = new Element("bind");
335		bind.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-bind");
336		iq.addChild(bind);
337		this.sendIqPacket(iq, new OnIqPacketReceived() {
338			@Override
339			public void onIqPacketReceived(Account account, IqPacket packet) {
340				String resource = packet.findChild("bind").findChild("jid")
341						.getContent().split("/")[1];
342				account.setResource(resource);
343				account.setStatus(Account.STATUS_ONLINE);
344				if (statusListener != null) {
345					statusListener.onStatusChanged(account);
346				}
347				sendServiceDiscovery();
348			}
349		});
350	}
351
352	private void sendServiceDiscovery() {
353		IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
354		iq.setAttribute("to", account.getServer());
355		Element query = new Element("query");
356		query.setAttribute("xmlns", "http://jabber.org/protocol/disco#info");
357		iq.addChild(query);
358		this.sendIqPacket(iq, new OnIqPacketReceived() {
359
360			@Override
361			public void onIqPacketReceived(Account account, IqPacket packet) {
362				if (packet.hasChild("query")) {
363					List<Element> elements = packet.findChild("query")
364							.getChildren();
365					for (int i = 0; i < elements.size(); ++i) {
366						if (elements.get(i).getName().equals("feature")) {
367							discoFeatures.add(elements.get(i).getAttribute(
368									"var"));
369						}
370					}
371				}
372				if (discoFeatures.contains("urn:xmpp:carbons:2")) {
373					sendEnableCarbons();
374				}
375			}
376		});
377	}
378	
379	private void sendEnableCarbons() {
380		Log.d(LOGTAG,account.getJid()+": enable carbons");
381		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
382		Element enable = new Element("enable");
383		enable.setAttribute("xmlns", "urn:xmpp:carbons:2");
384		iq.addChild(enable);
385		this.sendIqPacket(iq, new OnIqPacketReceived() {
386			
387			@Override
388			public void onIqPacketReceived(Account account, IqPacket packet) {
389				if (!packet.hasChild("error")) {
390					Log.d(LOGTAG,account.getJid()+": successfully enabled carbons");
391				} else {
392					Log.d(LOGTAG,account.getJid()+": error enableing carbons "+packet.toString());
393				}
394			}
395		});
396	}
397
398	private void processStreamError(Tag currentTag) {
399		Log.d(LOGTAG, "processStreamError");
400	}
401
402	private void sendStartStream() {
403		Tag stream = Tag.start("stream:stream");
404		stream.setAttribute("from", account.getJid());
405		stream.setAttribute("to", account.getServer());
406		stream.setAttribute("version", "1.0");
407		stream.setAttribute("xml:lang", "en");
408		stream.setAttribute("xmlns", "jabber:client");
409		stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
410		tagWriter.writeTag(stream);
411	}
412
413	private String nextRandomId() {
414		return new BigInteger(50, random).toString(32);
415	}
416
417	public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
418		String id = nextRandomId();
419		packet.setAttribute("id", id);
420		tagWriter.writeElement(packet);
421		if (callback != null) {
422			packetCallbacks.put(id, callback);
423		}
424	}
425
426	public void sendMessagePacket(MessagePacket packet) {
427		this.sendMessagePacket(packet, null);
428	}
429	
430	public void sendMessagePacket(MessagePacket packet, OnMessagePacketReceived callback) {
431		String id = nextRandomId();
432		packet.setAttribute("id", id);
433		tagWriter.writeElement(packet);
434		if (callback != null) {
435			packetCallbacks.put(id, callback);
436		}
437	}
438
439	public void sendPresencePacket(PresencePacket packet) {
440		this.sendPresencePacket(packet, null);
441	}
442	
443	public PresencePacket sendPresencePacket(PresencePacket packet, OnPresencePacketReceived callback) {
444		String id = nextRandomId();
445		packet.setAttribute("id", id);
446		tagWriter.writeElement(packet);
447		if (callback != null) {
448			packetCallbacks.put(id, callback);
449		}
450		return packet;
451	}
452
453	public void setOnMessagePacketReceivedListener(
454			OnMessagePacketReceived listener) {
455		this.messageListener = listener;
456	}
457
458	public void setOnUnregisteredIqPacketReceivedListener(
459			OnIqPacketReceived listener) {
460		this.unregisteredIqListener = listener;
461	}
462
463	public void setOnPresencePacketReceivedListener(
464			OnPresencePacketReceived listener) {
465		this.presenceListener = listener;
466	}
467
468	public void setOnStatusChangedListener(OnStatusChanged listener) {
469		this.statusListener = listener;
470	}
471
472	public void disconnect() {
473		shouldConnect = false;
474		tagWriter.writeTag(Tag.end("stream:stream"));
475	}
476}