XmppConnection.java

  1package de.gultsch.chat.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.Hashtable;
 11
 12import javax.net.ssl.SSLSocket;
 13import javax.net.ssl.SSLSocketFactory;
 14
 15import org.xmlpull.v1.XmlPullParserException;
 16
 17import android.os.Bundle;
 18import android.os.PowerManager;
 19import android.util.Log;
 20import de.gultsch.chat.entities.Account;
 21import de.gultsch.chat.utils.DNSHelper;
 22import de.gultsch.chat.utils.SASL;
 23import de.gultsch.chat.xml.Element;
 24import de.gultsch.chat.xml.Tag;
 25import de.gultsch.chat.xml.XmlReader;
 26import de.gultsch.chat.xml.TagWriter;
 27
 28public class XmppConnection implements Runnable {
 29
 30	protected Account account;
 31	private static final String LOGTAG = "xmppService";
 32
 33	private PowerManager.WakeLock wakeLock;
 34
 35	private SecureRandom random = new SecureRandom();
 36	
 37	private Socket socket;
 38	private XmlReader tagReader;
 39	private TagWriter tagWriter;
 40
 41	private boolean isTlsEncrypted = false;
 42	private boolean isAuthenticated = false;
 43	//private boolean shouldUseTLS = false;
 44	private boolean shouldConnect = true;
 45	private boolean shouldBind = true;
 46	private boolean shouldAuthenticate = true;
 47	private Element streamFeatures;
 48	
 49	private static final int PACKET_IQ = 0;
 50	private static final int PACKET_MESSAGE = 1;
 51	private static final int PACKET_PRESENCE = 2;
 52	
 53	private Hashtable<String, OnIqPacketReceived> iqPacketCallbacks = new Hashtable<String, OnIqPacketReceived>();
 54	private OnPresencePacketReceived presenceListener = null;
 55	private OnIqPacketReceived unregisteredIqListener = null;
 56	private OnMessagePacketReceived messageListener = null;
 57	private OnStatusChanged statusListener = null;
 58
 59	public XmppConnection(Account account, PowerManager pm) {
 60		this.account = account;
 61		wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
 62				"XmppConnection");
 63		tagReader = new XmlReader(wakeLock);
 64		tagWriter = new TagWriter();
 65	}
 66
 67	protected void connect() {
 68		try {
 69			Bundle namePort = DNSHelper.getSRVRecord(account.getServer());
 70			String srvRecordServer = namePort.getString("name");
 71			int srvRecordPort = namePort.getInt("port");
 72			if (srvRecordServer!=null) {
 73				Log.d(LOGTAG,account.getJid()+": using values from dns "+srvRecordServer+":"+srvRecordPort);
 74				socket = new Socket(srvRecordServer,srvRecordPort);
 75			} else {
 76				socket = new Socket(account.getServer(), 5222);
 77			}
 78			OutputStream out = socket.getOutputStream();
 79			tagWriter.setOutputStream(out);
 80			InputStream in = socket.getInputStream();
 81			tagReader.setInputStream(in);
 82			tagWriter.beginDocument();
 83			sendStartStream();
 84			Tag nextTag;
 85			while ((nextTag = tagReader.readTag()) != null) {
 86				if (nextTag.isStart("stream")) {
 87					processStream(nextTag);
 88					break;
 89				} else {
 90					Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName());
 91					return;
 92				}
 93			}
 94			if (socket.isConnected()) {
 95				socket.close();
 96			}
 97		} catch (UnknownHostException e) {
 98			account.setStatus(Account.STATUS_SERVER_NOT_FOUND);
 99			if (statusListener!=null) {
100				statusListener.onStatusChanged(account);
101			}
102			return;
103		} catch (IOException e) {
104			Log.d(LOGTAG,"bla "+e.getMessage());
105			if (shouldConnect) {
106				Log.d(LOGTAG,account.getJid()+": connection lost");
107				account.setStatus(Account.STATUS_OFFLINE);
108				if (statusListener!=null) {
109					statusListener.onStatusChanged(account);
110				}
111			}
112		} catch (XmlPullParserException e) {
113			Log.d(LOGTAG,"xml exception "+e.getMessage());
114			return;
115		}
116		
117	}
118
119	@Override
120	public void run() {
121		shouldConnect = true;
122		while(shouldConnect) {
123			connect();
124			try {
125				if (shouldConnect) {
126					Thread.sleep(30000);
127				}
128			} catch (InterruptedException e) {
129				// TODO Auto-generated catch block
130				e.printStackTrace();
131			}
132		}
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()+": read success tag in stream. reset again");
149				tagReader.readTag();
150				tagReader.reset();
151				sendStartStream();
152				processStream(tagReader.readTag());
153				break;
154			} else if(nextTag.isStart("failure")) {
155				Element failure = tagReader.readElement(nextTag);
156				Log.d(LOGTAG,"read failure element"+failure.toString());
157				account.setStatus(Account.STATUS_UNAUTHORIZED);
158				if (statusListener!=null) {
159					statusListener.onStatusChanged(account);
160				}
161				tagWriter.writeTag(Tag.end("stream"));
162			} else if (nextTag.isStart("iq")) {
163				processIq(nextTag);
164			} else if (nextTag.isStart("message")) {
165				processMessage(nextTag);
166			} else if (nextTag.isStart("presence")) {
167				processPresence(nextTag);
168			} else {
169				Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName()
170						+ " as child of " + currentTag.getName());
171			}
172			nextTag = tagReader.readTag();
173		}
174		if (account.getStatus() == Account.STATUS_ONLINE) {
175			account.setStatus(Account.STATUS_OFFLINE);
176			if (statusListener!=null) {
177				statusListener.onStatusChanged(account);
178			}
179		}
180	}
181	
182	private Element processPacket(Tag currentTag, int packetType) throws XmlPullParserException, IOException {
183		Element element;
184		switch (packetType) {
185		case PACKET_IQ:
186			element = new IqPacket();
187			break;
188		case PACKET_MESSAGE:
189			element = new MessagePacket();
190			break;
191		case PACKET_PRESENCE:
192			element = new PresencePacket();
193			break;
194		default:
195			return null;
196		}
197		element.setAttributes(currentTag.getAttributes());
198		Tag nextTag = tagReader.readTag();
199		while(!nextTag.isEnd(element.getName())) {
200			if (!nextTag.isNo()) {
201				Element child = tagReader.readElement(nextTag);
202				element.addChild(child);
203			}
204			nextTag = tagReader.readTag();
205		}
206		return element;
207	}
208	
209
210	private IqPacket processIq(Tag currentTag) throws XmlPullParserException, IOException {
211		IqPacket packet = (IqPacket) processPacket(currentTag,PACKET_IQ);
212		if (iqPacketCallbacks.containsKey(packet.getId())) {
213			iqPacketCallbacks.get(packet.getId()).onIqPacketReceived(account,packet);
214			iqPacketCallbacks.remove(packet.getId());
215		} else if (this.unregisteredIqListener != null) {
216			this.unregisteredIqListener.onIqPacketReceived(account,packet);
217		}
218		return packet;
219	}
220	
221	private void processMessage(Tag currentTag) throws XmlPullParserException, IOException {
222		MessagePacket packet = (MessagePacket) processPacket(currentTag, PACKET_MESSAGE);
223		if (this.messageListener != null) {
224			this.messageListener.onMessagePacketReceived(account,packet);
225		}
226	}
227	
228	private void processPresence(Tag currentTag) throws XmlPullParserException, IOException {
229		PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE);
230		if (this.presenceListener != null) {
231			this.presenceListener.onPresencePacketReceived(account,packet);
232		}
233	}
234
235	private void sendStartTLS() {
236		Tag startTLS = Tag.empty("starttls");
237		startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
238		Log.d(LOGTAG,account.getJid()+": sending starttls");
239		tagWriter.writeTag(startTLS);
240	}
241
242	private void switchOverToTls(Tag currentTag) throws XmlPullParserException,
243			IOException {
244		Tag nextTag = tagReader.readTag(); // should be proceed end tag
245		Log.d(LOGTAG,account.getJid()+": now switch to ssl");
246		SSLSocket sslSocket;
247		try {
248			sslSocket = (SSLSocket) ((SSLSocketFactory) SSLSocketFactory
249					.getDefault()).createSocket(socket, socket.getInetAddress()
250					.getHostAddress(), socket.getPort(), true);
251			tagReader.setInputStream(sslSocket.getInputStream());
252			Log.d(LOGTAG, "reset inputstream");
253			tagWriter.setOutputStream(sslSocket.getOutputStream());
254			Log.d(LOGTAG, "switch over seemed to work");
255			isTlsEncrypted = true;
256			sendStartStream();
257			processStream(tagReader.readTag());
258			sslSocket.close();
259		} catch (IOException e) {
260			Log.d(LOGTAG, account.getJid()+": error on ssl '" + e.getMessage()+"'");
261		}
262	}
263
264	private void sendSaslAuth() throws IOException, XmlPullParserException {
265		String saslString = SASL.plain(account.getUsername(),
266				account.getPassword());
267		Element auth = new Element("auth");
268		auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
269		auth.setAttribute("mechanism", "PLAIN");
270		auth.setContent(saslString);
271		Log.d(LOGTAG,account.getJid()+": sending sasl "+auth.toString());
272		tagWriter.writeElement(auth);
273	}
274
275	private void processStreamFeatures(Tag currentTag)
276			throws XmlPullParserException, IOException {
277		this.streamFeatures = tagReader.readElement(currentTag);
278		Log.d(LOGTAG,account.getJid()+": process stream features "+streamFeatures);
279		if (this.streamFeatures.hasChild("starttls")&&account.isOptionSet(Account.OPTION_USETLS)) {
280			sendStartTLS();
281		} else if (this.streamFeatures.hasChild("mechanisms")&&shouldAuthenticate) {
282			sendSaslAuth();
283		}
284		if (this.streamFeatures.hasChild("bind")&&shouldBind) {
285			sendBindRequest();
286			if (this.streamFeatures.hasChild("session")) {
287				IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
288				Element session = new Element("session");
289				session.setAttribute("xmlns","urn:ietf:params:xml:ns:xmpp-session");
290				session.setContent("");
291				startSession.addChild(session);
292				sendIqPacket(startSession, null);
293				tagWriter.writeElement(startSession);
294			}
295			Element presence = new Element("presence");
296			
297			tagWriter.writeElement(presence);
298		}
299	}
300
301	private void sendBindRequest() throws IOException {
302		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
303		Element bind = new Element("bind");
304		bind.setAttribute("xmlns","urn:ietf:params:xml:ns:xmpp-bind");
305		iq.addChild(bind);
306		this.sendIqPacket(iq, new OnIqPacketReceived() {	
307			@Override
308			public void onIqPacketReceived(Account account, IqPacket packet) {
309				String resource = packet.findChild("bind").findChild("jid").getContent().split("/")[1];
310				account.setResource(resource);
311				account.setStatus(Account.STATUS_ONLINE);
312				if (statusListener!=null) {
313					statusListener.onStatusChanged(account);
314				}
315			}
316		});
317	}
318
319	private void processStreamError(Tag currentTag) {
320		Log.d(LOGTAG, "processStreamError");
321	}
322
323	private void sendStartStream() {
324		Tag stream = Tag.start("stream");
325		stream.setAttribute("from", account.getJid());
326		stream.setAttribute("to", account.getServer());
327		stream.setAttribute("version", "1.0");
328		stream.setAttribute("xml:lang", "en");
329		stream.setAttribute("xmlns", "jabber:client");
330		stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
331		tagWriter.writeTag(stream);
332	}
333
334	private String nextRandomId() {
335		return new BigInteger(50, random).toString(32);
336	}
337	
338	public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
339		String id = nextRandomId();
340		packet.setAttribute("id",id);
341		tagWriter.writeElement(packet);
342		if (callback != null) {
343			iqPacketCallbacks.put(id, callback);
344		}
345		Log.d(LOGTAG,account.getJid()+": sending: "+packet.toString());
346	}
347	
348	public void sendMessagePacket(MessagePacket packet){
349		tagWriter.writeElement(packet);
350	}
351	
352	public void sendPresencePacket(PresencePacket packet)  {
353		tagWriter.writeElement(packet);
354		Log.d(LOGTAG,account.getJid()+": sending: "+packet.toString());
355	}
356	
357	public void setOnMessagePacketReceivedListener(OnMessagePacketReceived listener) {
358		this.messageListener = listener;
359	}
360	
361	public void setOnUnregisteredIqPacketReceivedListener(OnIqPacketReceived listener) {
362		this.unregisteredIqListener = listener;
363	}
364	
365	public void setOnPresencePacketReceivedListener(OnPresencePacketReceived listener) {
366		this.presenceListener = listener;
367	}
368	
369	public void setOnStatusChangedListener(OnStatusChanged listener) {
370		this.statusListener = listener;
371	}
372
373	public void disconnect() {
374		shouldConnect = false;
375		tagWriter.writeTag(Tag.end("stream"));
376	}
377}