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