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.KeyManagementException;
 10import java.security.KeyStore;
 11import java.security.KeyStoreException;
 12import java.security.MessageDigest;
 13import java.security.NoSuchAlgorithmException;
 14import java.security.SecureRandom;
 15import java.security.cert.CertPathValidatorException;
 16import java.security.cert.CertificateException;
 17import java.security.cert.X509Certificate;
 18import java.util.HashSet;
 19import java.util.Hashtable;
 20import java.util.List;
 21
 22import javax.net.ssl.SSLContext;
 23import javax.net.ssl.SSLSocket;
 24import javax.net.ssl.SSLSocketFactory;
 25import javax.net.ssl.TrustManager;
 26import javax.net.ssl.TrustManagerFactory;
 27import javax.net.ssl.X509TrustManager;
 28
 29import org.json.JSONException;
 30import org.xmlpull.v1.XmlPullParserException;
 31
 32import android.content.IntentSender.SendIntentException;
 33import android.os.Bundle;
 34import android.os.PowerManager;
 35import android.util.Log;
 36import eu.siacs.conversations.entities.Account;
 37import eu.siacs.conversations.utils.CryptoHelper;
 38import eu.siacs.conversations.utils.DNSHelper;
 39import eu.siacs.conversations.xml.Element;
 40import eu.siacs.conversations.xml.Tag;
 41import eu.siacs.conversations.xml.TagWriter;
 42import eu.siacs.conversations.xml.XmlReader;
 43import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
 44import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 45import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 46import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
 47import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
 48import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
 49import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
 50import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
 51
 52public class XmppConnection implements Runnable {
 53
 54	protected Account account;
 55	private static final String LOGTAG = "xmppService";
 56
 57	private PowerManager.WakeLock wakeLock;
 58
 59	private SecureRandom random = new SecureRandom();
 60
 61	private Socket socket;
 62	private XmlReader tagReader;
 63	private TagWriter tagWriter;
 64
 65	private boolean shouldBind = true;
 66	private boolean shouldAuthenticate = true;
 67	private Element streamFeatures;
 68	private HashSet<String> discoFeatures = new HashSet<String>();
 69	
 70	private String streamId = null;
 71	
 72	private int stanzasReceived = 0;
 73	private int stanzasSent = 0;
 74
 75	private static final int PACKET_IQ = 0;
 76	private static final int PACKET_MESSAGE = 1;
 77	private static final int PACKET_PRESENCE = 2;
 78
 79	private Hashtable<String, PacketReceived> packetCallbacks = new Hashtable<String, PacketReceived>();
 80	private OnPresencePacketReceived presenceListener = null;
 81	private OnIqPacketReceived unregisteredIqListener = null;
 82	private OnMessagePacketReceived messageListener = null;
 83	private OnStatusChanged statusListener = null;
 84	private OnTLSExceptionReceived tlsListener;
 85
 86	public XmppConnection(Account account, PowerManager pm) {
 87		this.account = account;
 88		wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
 89				"XmppConnection");
 90		tagReader = new XmlReader(wakeLock);
 91		tagWriter = new TagWriter();
 92	}
 93
 94	protected void changeStatus(int nextStatus) {
 95		account.setStatus(nextStatus);
 96		if (statusListener != null) {
 97			statusListener.onStatusChanged(account);
 98		}
 99	}
100
101	protected void connect() {
102		Log.d(LOGTAG, "connecting");
103		try {
104			tagReader = new XmlReader(wakeLock);
105			tagWriter = new TagWriter();
106			packetCallbacks.clear();
107			this.changeStatus(Account.STATUS_CONNECTING);
108			Bundle namePort = DNSHelper.getSRVRecord(account.getServer());
109			String srvRecordServer = namePort.getString("name");
110			int srvRecordPort = namePort.getInt("port");
111			if (srvRecordServer != null) {
112				Log.d(LOGTAG, account.getJid() + ": using values from dns "
113						+ srvRecordServer + ":" + srvRecordPort);
114				socket = new Socket(srvRecordServer, srvRecordPort);
115			} else {
116				socket = new Socket(account.getServer(), 5222);
117			}
118			OutputStream out = socket.getOutputStream();
119			tagWriter.setOutputStream(out);
120			InputStream in = socket.getInputStream();
121			tagReader.setInputStream(in);
122			tagWriter.beginDocument();
123			sendStartStream();
124			Tag nextTag;
125			while ((nextTag = tagReader.readTag()) != null) {
126				if (nextTag.isStart("stream")) {
127					processStream(nextTag);
128					break;
129				} else {
130					Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName());
131					return;
132				}
133			}
134			if (socket.isConnected()) {
135				socket.close();
136			}
137		} catch (UnknownHostException e) {
138			this.changeStatus(Account.STATUS_SERVER_NOT_FOUND);
139			if (wakeLock.isHeld()) {
140				wakeLock.release();
141			}
142			return;
143		} catch (IOException e) {
144			if (account.getStatus() != Account.STATUS_TLS_ERROR) {
145				this.changeStatus(Account.STATUS_OFFLINE);
146			}
147			if (wakeLock.isHeld()) {
148				wakeLock.release();
149			}
150			return;
151		} catch (XmlPullParserException e) {
152			this.changeStatus(Account.STATUS_OFFLINE);
153			Log.d(LOGTAG, "xml exception " + e.getMessage());
154			if (wakeLock.isHeld()) {
155				wakeLock.release();
156			}
157			return;
158		}
159
160	}
161
162	@Override
163	public void run() {
164		connect();
165		Log.d(LOGTAG, "end run");
166	}
167
168	private void processStream(Tag currentTag) throws XmlPullParserException,
169			IOException {
170		Tag nextTag = tagReader.readTag();
171		while ((nextTag != null) && (!nextTag.isEnd("stream"))) {
172			if (nextTag.isStart("error")) {
173				processStreamError(nextTag);
174			} else if (nextTag.isStart("features")) {
175				processStreamFeatures(nextTag);
176				if ((streamFeatures.getChildren().size() == 1)
177						&& (streamFeatures.hasChild("starttls"))
178						&& (!account.isOptionSet(Account.OPTION_USETLS))) {
179					changeStatus(Account.STATUS_SERVER_REQUIRES_TLS);
180				}
181			} else if (nextTag.isStart("proceed")) {
182				switchOverToTls(nextTag);
183			} else if (nextTag.isStart("success")) {
184				Log.d(LOGTAG, account.getJid()
185						+ ": logged in");
186				tagReader.readTag();
187				tagReader.reset();
188				sendStartStream();
189				processStream(tagReader.readTag());
190				break;
191			} else if (nextTag.isStart("failure")) {
192				Element failure = tagReader.readElement(nextTag);
193				changeStatus(Account.STATUS_UNAUTHORIZED);
194			} else if (nextTag.isStart("enabled")) {
195				this.stanzasSent = 0;
196				Element enabled = tagReader.readElement(nextTag);
197				if ("true".equals(enabled.getAttribute("resume"))) {
198					this.streamId = enabled.getAttribute("id");
199					Log.d(LOGTAG,account.getJid()+": stream managment enabled (resumable)");
200				} else {
201					Log.d(LOGTAG,account.getJid()+": stream managment enabled");
202				}
203				this.stanzasReceived = 0;
204				RequestPacket r = new RequestPacket();
205				tagWriter.writeStanzaAsync(r);
206			} else if (nextTag.isStart("resumed")) {
207				tagReader.readElement(nextTag);
208				changeStatus(Account.STATUS_ONLINE);
209				Log.d(LOGTAG,account.getJid()+": session resumed");
210			} else if (nextTag.isStart("r")) {
211				tagReader.readElement(nextTag);
212				AckPacket ack = new AckPacket(this.stanzasReceived);
213				//Log.d(LOGTAG,ack.toString());
214				tagWriter.writeStanzaAsync(ack);
215			} else if (nextTag.isStart("a")) {
216				Element ack = tagReader.readElement(nextTag);
217				int serverSequence = Integer.parseInt(ack.getAttribute("h"));
218				if (serverSequence>this.stanzasSent) {
219					this.stanzasSent = serverSequence;
220				}
221				//Log.d(LOGTAG,"server ack"+ack.toString()+" ("+this.stanzasSent+")");
222			} else if (nextTag.isStart("iq")) {
223				processIq(nextTag);
224			} else if (nextTag.isStart("message")) {
225				processMessage(nextTag);
226			} else if (nextTag.isStart("presence")) {
227				processPresence(nextTag);
228			} else {
229				Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName()
230						+ " as child of " + currentTag.getName());
231			}
232			nextTag = tagReader.readTag();
233		}
234		if (account.getStatus() == Account.STATUS_ONLINE) {
235			account.setStatus(Account.STATUS_OFFLINE);
236			if (statusListener != null) {
237				statusListener.onStatusChanged(account);
238			}
239		}
240	}
241
242	private Element processPacket(Tag currentTag, int packetType)
243			throws XmlPullParserException, IOException {
244		Element element;
245		switch (packetType) {
246		case PACKET_IQ:
247			element = new IqPacket();
248			break;
249		case PACKET_MESSAGE:
250			element = new MessagePacket();
251			break;
252		case PACKET_PRESENCE:
253			element = new PresencePacket();
254			break;
255		default:
256			return null;
257		}
258		element.setAttributes(currentTag.getAttributes());
259		Tag nextTag = tagReader.readTag();
260		while (!nextTag.isEnd(element.getName())) {
261			if (!nextTag.isNo()) {
262				Element child = tagReader.readElement(nextTag);
263				element.addChild(child);
264			}
265			nextTag = tagReader.readTag();
266		}
267		++stanzasReceived;
268		return element;
269	}
270
271	private void processIq(Tag currentTag) throws XmlPullParserException,
272			IOException {
273		IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
274		if (packetCallbacks.containsKey(packet.getId())) {
275			if (packetCallbacks.get(packet.getId()) instanceof OnIqPacketReceived) {
276				((OnIqPacketReceived) packetCallbacks.get(packet.getId()))
277						.onIqPacketReceived(account, packet);
278			}
279
280			packetCallbacks.remove(packet.getId());
281		} else if (this.unregisteredIqListener != null) {
282			this.unregisteredIqListener.onIqPacketReceived(account, packet);
283		}
284	}
285
286	private void processMessage(Tag currentTag) throws XmlPullParserException,
287			IOException {
288		MessagePacket packet = (MessagePacket) processPacket(currentTag,
289				PACKET_MESSAGE);
290		String id = packet.getAttribute("id");
291		if ((id != null) && (packetCallbacks.containsKey(id))) {
292			if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) {
293				((OnMessagePacketReceived) packetCallbacks.get(id))
294						.onMessagePacketReceived(account, packet);
295			}
296			packetCallbacks.remove(id);
297		} else if (this.messageListener != null) {
298			this.messageListener.onMessagePacketReceived(account, packet);
299		}
300	}
301
302	private void processPresence(Tag currentTag) throws XmlPullParserException,
303			IOException {
304		PresencePacket packet = (PresencePacket) processPacket(currentTag,
305				PACKET_PRESENCE);
306		String id = packet.getAttribute("id");
307		if ((id != null) && (packetCallbacks.containsKey(id))) {
308			if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) {
309				((OnPresencePacketReceived) packetCallbacks.get(id))
310						.onPresencePacketReceived(account, packet);
311			}
312			packetCallbacks.remove(id);
313		} else if (this.presenceListener != null) {
314			this.presenceListener.onPresencePacketReceived(account, packet);
315		}
316	}
317
318	private void sendStartTLS() throws IOException {
319		Tag startTLS = Tag.empty("starttls");
320		startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
321		tagWriter.writeTag(startTLS);
322	}
323
324	private void switchOverToTls(Tag currentTag) throws XmlPullParserException,
325			IOException {
326		Tag nextTag = tagReader.readTag(); // should be proceed end tag
327		try {
328			SSLContext sc = SSLContext.getInstance("TLS");
329			TrustManagerFactory tmf = TrustManagerFactory
330					.getInstance(TrustManagerFactory.getDefaultAlgorithm());
331			// Initialise the TMF as you normally would, for example:
332			// tmf.in
333			try {
334				tmf.init((KeyStore) null);
335			} catch (KeyStoreException e1) {
336				// TODO Auto-generated catch block
337				e1.printStackTrace();
338			}
339
340			TrustManager[] trustManagers = tmf.getTrustManagers();
341			final X509TrustManager origTrustmanager = (X509TrustManager) trustManagers[0];
342
343			TrustManager[] wrappedTrustManagers = new TrustManager[] { new X509TrustManager() {
344
345				@Override
346				public void checkClientTrusted(X509Certificate[] chain,
347						String authType) throws CertificateException {
348					origTrustmanager.checkClientTrusted(chain, authType);
349				}
350
351				@Override
352				public void checkServerTrusted(X509Certificate[] chain,
353						String authType) throws CertificateException {
354					try {
355						origTrustmanager.checkServerTrusted(chain, authType);
356					} catch (CertificateException e) {
357						if (e.getCause() instanceof CertPathValidatorException) {
358							String sha;
359							try {
360								MessageDigest sha1 = MessageDigest.getInstance("SHA1");
361								sha1.update(chain[0].getEncoded());
362								sha = CryptoHelper.bytesToHex(sha1.digest());
363								if (!sha.equals(account.getSSLFingerprint())) {
364									changeStatus(Account.STATUS_TLS_ERROR);
365									if (tlsListener!=null) {
366										tlsListener.onTLSExceptionReceived(sha,account);
367									}
368									throw new CertificateException();
369								}
370							} catch (NoSuchAlgorithmException e1) {
371								// TODO Auto-generated catch block
372								e1.printStackTrace();
373							}
374						} else {
375							throw new CertificateException();
376						}
377					}
378				}
379
380				@Override
381				public X509Certificate[] getAcceptedIssuers() {
382					return origTrustmanager.getAcceptedIssuers();
383				}
384
385			} };
386			sc.init(null, wrappedTrustManagers, null);
387			SSLSocketFactory factory = sc.getSocketFactory();
388			SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,
389						socket.getInetAddress().getHostAddress(), socket.getPort(),
390						true);
391			tagReader.setInputStream(sslSocket.getInputStream());
392			tagWriter.setOutputStream(sslSocket.getOutputStream());
393			sendStartStream();
394			Log.d(LOGTAG,account.getJid()+": TLS connection established");
395			processStream(tagReader.readTag());
396			sslSocket.close();
397		} catch (NoSuchAlgorithmException e1) {
398			// TODO Auto-generated catch block
399			e1.printStackTrace();
400		} catch (KeyManagementException e) {
401			// TODO Auto-generated catch block
402			e.printStackTrace();
403		}
404	}
405
406	private void sendSaslAuth() throws IOException, XmlPullParserException {
407		String saslString = CryptoHelper.saslPlain(account.getUsername(),
408				account.getPassword());
409		Element auth = new Element("auth");
410		auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
411		auth.setAttribute("mechanism", "PLAIN");
412		auth.setContent(saslString);
413		tagWriter.writeElement(auth);
414	}
415
416	private void processStreamFeatures(Tag currentTag)
417			throws XmlPullParserException, IOException {
418		this.streamFeatures = tagReader.readElement(currentTag);
419		if (this.streamFeatures.hasChild("starttls")
420				&& account.isOptionSet(Account.OPTION_USETLS)) {
421			sendStartTLS();
422		} else if (this.streamFeatures.hasChild("mechanisms")
423				&& shouldAuthenticate) {
424			sendSaslAuth();
425		} else if (this.streamFeatures.hasChild("sm") && streamId != null) {
426			Log.d(LOGTAG,"found old stream id. trying to remuse");
427			ResumePacket resume = new ResumePacket(this.streamId,stanzasReceived);
428			this.tagWriter.writeStanzaAsync(resume);
429		} else if (this.streamFeatures.hasChild("bind") && shouldBind) {
430			sendBindRequest();
431			if (this.streamFeatures.hasChild("session")) {
432				Log.d(LOGTAG,"sending session");
433				IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
434				Element session = new Element("session");
435				session.setAttribute("xmlns",
436						"urn:ietf:params:xml:ns:xmpp-session");
437				session.setContent("");
438				startSession.addChild(session);
439				this.sendIqPacket(startSession, null);
440			}
441		}
442	}
443
444	private void sendInitialPresence() {
445		PresencePacket packet = new PresencePacket();
446		packet.setAttribute("from", account.getFullJid());
447		if (account.getKeys().has("pgp_signature")) {
448			try {
449				String signature = account.getKeys().getString("pgp_signature");	
450				Element status = new Element("status");
451				status.setContent("online");
452				packet.addChild(status);
453				Element x = new Element("x");
454				x.setAttribute("xmlns", "jabber:x:signed");
455				x.setContent(signature);
456				packet.addChild(x);
457			} catch (JSONException e) {
458				//
459			}
460		}
461		this.sendPresencePacket(packet);
462	}
463
464	private void sendBindRequest() throws IOException {
465		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
466		Element bind = new Element("bind");
467		bind.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-bind");
468		Element resource = new Element("resource");
469		resource.setContent("Conversations");
470		bind.addChild(resource);
471		iq.addChild(bind);
472		this.sendIqPacket(iq, new OnIqPacketReceived() {
473			@Override
474			public void onIqPacketReceived(Account account, IqPacket packet) {
475				String resource = packet.findChild("bind").findChild("jid")
476						.getContent().split("/")[1];
477				account.setResource(resource);
478				account.setStatus(Account.STATUS_ONLINE);
479				if (streamFeatures.hasChild("sm")) {
480					EnablePacket enable = new EnablePacket();
481					tagWriter.writeStanzaAsync(enable);
482				}
483				sendInitialPresence();
484				sendServiceDiscovery();
485				if (statusListener != null) {
486					statusListener.onStatusChanged(account);
487				}
488			}
489		});
490	}
491
492	private void sendServiceDiscovery() {
493		IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
494		iq.setAttribute("to", account.getServer());
495		Element query = new Element("query");
496		query.setAttribute("xmlns", "http://jabber.org/protocol/disco#info");
497		iq.addChild(query);
498		this.sendIqPacket(iq, new OnIqPacketReceived() {
499
500			@Override
501			public void onIqPacketReceived(Account account, IqPacket packet) {
502				if (packet.hasChild("query")) {
503					List<Element> elements = packet.findChild("query")
504							.getChildren();
505					for (int i = 0; i < elements.size(); ++i) {
506						if (elements.get(i).getName().equals("feature")) {
507							discoFeatures.add(elements.get(i).getAttribute(
508									"var"));
509						}
510					}
511				}
512				if (discoFeatures.contains("urn:xmpp:carbons:2")) {
513					sendEnableCarbons();
514				}
515			}
516		});
517	}
518
519	private void sendEnableCarbons() {
520		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
521		Element enable = new Element("enable");
522		enable.setAttribute("xmlns", "urn:xmpp:carbons:2");
523		iq.addChild(enable);
524		this.sendIqPacket(iq, new OnIqPacketReceived() {
525
526			@Override
527			public void onIqPacketReceived(Account account, IqPacket packet) {
528				if (!packet.hasChild("error")) {
529					Log.d(LOGTAG, account.getJid()
530							+ ": successfully enabled carbons");
531				} else {
532					Log.d(LOGTAG, account.getJid()
533							+ ": error enableing carbons " + packet.toString());
534				}
535			}
536		});
537	}
538
539	private void processStreamError(Tag currentTag) {
540		Log.d(LOGTAG, "processStreamError");
541	}
542
543	private void sendStartStream() throws IOException {
544		Tag stream = Tag.start("stream:stream");
545		stream.setAttribute("from", account.getJid());
546		stream.setAttribute("to", account.getServer());
547		stream.setAttribute("version", "1.0");
548		stream.setAttribute("xml:lang", "en");
549		stream.setAttribute("xmlns", "jabber:client");
550		stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
551		tagWriter.writeTag(stream);
552	}
553
554	private String nextRandomId() {
555		return new BigInteger(50, random).toString(32);
556	}
557
558	public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
559		String id = nextRandomId();
560		packet.setAttribute("id", id);
561		this.sendPacket(packet, callback);
562	}
563
564	public void sendMessagePacket(MessagePacket packet) {
565		this.sendPacket(packet, null);
566	}
567
568	public void sendMessagePacket(MessagePacket packet,
569			OnMessagePacketReceived callback) {
570		this.sendPacket(packet, callback);
571	}
572
573	public void sendPresencePacket(PresencePacket packet) {
574		this.sendPacket(packet, null);
575	}
576
577	public void sendPresencePacket(PresencePacket packet,
578			OnPresencePacketReceived callback) {
579		this.sendPacket(packet, callback);
580	}
581	
582	private synchronized void sendPacket(final AbstractStanza packet, PacketReceived callback) {
583		++stanzasSent;
584		tagWriter.writeStanzaAsync(packet);
585		if (callback != null) {
586			if (packet.getId()==null) {
587				packet.setId(nextRandomId());
588			}
589			packetCallbacks.put(packet.getId(), callback);
590		}
591	}
592
593	public void setOnMessagePacketReceivedListener(
594			OnMessagePacketReceived listener) {
595		this.messageListener = listener;
596	}
597
598	public void setOnUnregisteredIqPacketReceivedListener(
599			OnIqPacketReceived listener) {
600		this.unregisteredIqListener = listener;
601	}
602
603	public void setOnPresencePacketReceivedListener(
604			OnPresencePacketReceived listener) {
605		this.presenceListener = listener;
606	}
607
608	public void setOnStatusChangedListener(OnStatusChanged listener) {
609		this.statusListener = listener;
610	}
611	
612	public void setOnTLSExceptionReceivedListener(OnTLSExceptionReceived listener) {
613		this.tlsListener = listener;
614	}
615
616	public void disconnect(boolean force) {
617		Log.d(LOGTAG,"disconnecting");
618		try {
619		if (force) {
620				socket.close();
621		}
622		tagWriter.finish();
623		while(!tagWriter.finished()) {
624			Log.d(LOGTAG,"not yet finished");
625			Thread.sleep(100);
626		}
627		tagWriter.writeTag(Tag.end("stream:stream"));
628		} catch (IOException e) {
629			Log.d(LOGTAG,"io exception during disconnect");
630		} catch (InterruptedException e) {
631			Log.d(LOGTAG,"interupted while waiting for disconnect");
632		}
633	}
634	
635	public boolean hasFeatureRosterManagment() {
636		if (this.streamFeatures==null) {
637			return false;
638		} else {
639			return this.streamFeatures.hasChild("ver");
640		}
641	}
642
643	public void r() {
644		this.tagWriter.writeStanzaAsync(new RequestPacket());
645	}
646}