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