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