1package eu.siacs.conversations.xmpp;
2
3import android.content.Context;
4import android.content.SharedPreferences;
5import android.os.Bundle;
6import android.os.Parcelable;
7import android.os.PowerManager;
8import android.os.PowerManager.WakeLock;
9import android.os.SystemClock;
10import android.preference.PreferenceManager;
11import android.util.Log;
12import android.util.SparseArray;
13
14import org.apache.http.conn.ssl.StrictHostnameVerifier;
15import org.json.JSONException;
16import org.json.JSONObject;
17import org.xmlpull.v1.XmlPullParserException;
18
19import java.io.IOException;
20import java.io.InputStream;
21import java.io.OutputStream;
22import java.math.BigInteger;
23import java.net.ConnectException;
24import java.net.IDN;
25import java.net.InetAddress;
26import java.net.InetSocketAddress;
27import java.net.Socket;
28import java.net.UnknownHostException;
29import java.security.KeyManagementException;
30import java.security.NoSuchAlgorithmException;
31import java.util.ArrayList;
32import java.util.Arrays;
33import java.util.HashMap;
34import java.util.Hashtable;
35import java.util.LinkedList;
36import java.util.List;
37import java.util.Map.Entry;
38
39import javax.net.ssl.HostnameVerifier;
40import javax.net.ssl.SSLContext;
41import javax.net.ssl.SSLSocket;
42import javax.net.ssl.SSLSocketFactory;
43import javax.net.ssl.X509TrustManager;
44
45import eu.siacs.conversations.Config;
46import eu.siacs.conversations.crypto.sasl.DigestMd5;
47import eu.siacs.conversations.crypto.sasl.Plain;
48import eu.siacs.conversations.crypto.sasl.SaslMechanism;
49import eu.siacs.conversations.crypto.sasl.ScramSha1;
50import eu.siacs.conversations.entities.Account;
51import eu.siacs.conversations.services.XmppConnectionService;
52import eu.siacs.conversations.utils.DNSHelper;
53import eu.siacs.conversations.xml.Element;
54import eu.siacs.conversations.xml.Tag;
55import eu.siacs.conversations.xml.TagWriter;
56import eu.siacs.conversations.xml.XmlReader;
57import eu.siacs.conversations.xmpp.jid.InvalidJidException;
58import eu.siacs.conversations.xmpp.jid.Jid;
59import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
60import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
61import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
62import eu.siacs.conversations.xmpp.stanzas.IqPacket;
63import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
64import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
65import eu.siacs.conversations.xmpp.stanzas.csi.ActivePacket;
66import eu.siacs.conversations.xmpp.stanzas.csi.InactivePacket;
67import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
68import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
69import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
70import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
71
72public class XmppConnection implements Runnable {
73
74 private static final int PACKET_IQ = 0;
75 private static final int PACKET_MESSAGE = 1;
76 private static final int PACKET_PRESENCE = 2;
77 private final Context applicationContext;
78 protected Account account;
79 private WakeLock wakeLock;
80 private Socket socket;
81 private XmlReader tagReader;
82 private TagWriter tagWriter;
83 private Features features = new Features(this);
84 private boolean shouldBind = true;
85 private boolean shouldAuthenticate = true;
86 private Element streamFeatures;
87 private HashMap<String, List<String>> disco = new HashMap<>();
88
89 private String streamId = null;
90 private int smVersion = 3;
91 private SparseArray<String> messageReceipts = new SparseArray<>();
92
93 private boolean enabledEncryption = false;
94 private boolean enabledCarbons = false;
95
96 private int stanzasReceived = 0;
97 private int stanzasSent = 0;
98 private long lastPaketReceived = 0;
99 private long lastPingSent = 0;
100 private long lastConnect = 0;
101 private long lastSessionStarted = 0;
102 private int attempt = 0;
103 private Hashtable<String, PacketReceived> packetCallbacks = new Hashtable<>();
104 private OnPresencePacketReceived presenceListener = null;
105 private OnJinglePacketReceived jingleListener = null;
106 private OnIqPacketReceived unregisteredIqListener = null;
107 private OnMessagePacketReceived messageListener = null;
108 private OnStatusChanged statusListener = null;
109 private OnBindListener bindListener = null;
110 private OnMessageAcknowledged acknowledgedListener = null;
111 private XmppConnectionService mXmppConnectionService = null;
112
113 private SaslMechanism saslMechanism;
114
115 public XmppConnection(Account account, XmppConnectionService service) {
116 this.account = account;
117 this.wakeLock = service.getPowerManager().newWakeLock(
118 PowerManager.PARTIAL_WAKE_LOCK, account.getJid().toBareJid().toString());
119 tagWriter = new TagWriter();
120 mXmppConnectionService = service;
121 applicationContext = service.getApplicationContext();
122 }
123
124 protected void changeStatus(final Account.State nextStatus) {
125 if (account.getStatus() != nextStatus) {
126 if ((nextStatus == Account.State.OFFLINE)
127 && (account.getStatus() != Account.State.CONNECTING)
128 && (account.getStatus() != Account.State.ONLINE)
129 && (account.getStatus() != Account.State.DISABLED)) {
130 return;
131 }
132 if (nextStatus == Account.State.ONLINE) {
133 this.attempt = 0;
134 }
135 account.setStatus(nextStatus);
136 if (statusListener != null) {
137 statusListener.onStatusChanged(account);
138 }
139 }
140 }
141
142 protected void connect() {
143 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": connecting");
144 enabledEncryption = false;
145 lastConnect = SystemClock.elapsedRealtime();
146 lastPingSent = SystemClock.elapsedRealtime();
147 this.attempt++;
148 try {
149 shouldAuthenticate = shouldBind = !account
150 .isOptionSet(Account.OPTION_REGISTER);
151 tagReader = new XmlReader(wakeLock);
152 tagWriter = new TagWriter();
153 packetCallbacks.clear();
154 this.changeStatus(Account.State.CONNECTING);
155 Bundle result = DNSHelper.getSRVRecord(account.getServer());
156 ArrayList<Parcelable> values = result.getParcelableArrayList("values");
157 if ("timeout".equals(result.getString("error"))) {
158 throw new IOException("timeout in dns");
159 } else if (values != null) {
160 int i = 0;
161 boolean socketError = true;
162 while (socketError && values.size() > i) {
163 Bundle namePort = (Bundle) values.get(i);
164 try {
165 String srvRecordServer;
166 try {
167 srvRecordServer=IDN.toASCII(namePort.getString("name"));
168 } catch (final IllegalArgumentException e) {
169 // TODO: Handle me?`
170 srvRecordServer = "";
171 }
172 int srvRecordPort = namePort.getInt("port");
173 String srvIpServer = namePort.getString("ipv4");
174 InetSocketAddress addr;
175 if (srvIpServer != null) {
176 addr = new InetSocketAddress(srvIpServer, srvRecordPort);
177 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
178 + ": using values from dns " + srvRecordServer
179 + "[" + srvIpServer + "]:" + srvRecordPort);
180 } else {
181 addr = new InetSocketAddress(srvRecordServer, srvRecordPort);
182 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
183 + ": using values from dns "
184 + srvRecordServer + ":" + srvRecordPort);
185 }
186 socket = new Socket();
187 socket.connect(addr, 20000);
188 socketError = false;
189 } catch (UnknownHostException e) {
190 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage());
191 i++;
192 } catch (IOException e) {
193 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage());
194 i++;
195 }
196 }
197 if (socketError) {
198 throw new UnknownHostException();
199 }
200 } else if (result.containsKey("error")
201 && "nosrv".equals(result.getString("error", null))) {
202 socket = new Socket(account.getServer().getDomainpart(), 5222);
203 } else {
204 throw new IOException("timeout in dns");
205 }
206 OutputStream out = socket.getOutputStream();
207 tagWriter.setOutputStream(out);
208 InputStream in = socket.getInputStream();
209 tagReader.setInputStream(in);
210 tagWriter.beginDocument();
211 sendStartStream();
212 Tag nextTag;
213 while ((nextTag = tagReader.readTag()) != null) {
214 if (nextTag.isStart("stream")) {
215 processStream(nextTag);
216 break;
217 } else {
218 throw new IOException("unknown tag on connect");
219 }
220 }
221 if (socket.isConnected()) {
222 socket.close();
223 }
224 } catch (UnknownHostException e) {
225 this.changeStatus(Account.State.SERVER_NOT_FOUND);
226 } catch (final ConnectException e) {
227 this.changeStatus(Account.State.SERVER_NOT_FOUND);
228 } catch (final IOException | XmlPullParserException e) {
229 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage());
230 this.changeStatus(Account.State.OFFLINE);
231 } catch (NoSuchAlgorithmException e) {
232 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage());
233 this.changeStatus(Account.State.OFFLINE);
234 } finally {
235 if (wakeLock.isHeld()) {
236 try {
237 wakeLock.release();
238 } catch (final RuntimeException ignored) {
239 }
240 }
241 }
242 }
243
244 @Override
245 public void run() {
246 connect();
247 }
248
249 private void processStream(final Tag currentTag) throws XmlPullParserException,
250 IOException, NoSuchAlgorithmException {
251 Tag nextTag = tagReader.readTag();
252
253 while ((nextTag != null) && (!nextTag.isEnd("stream"))) {
254 if (nextTag.isStart("error")) {
255 processStreamError(nextTag);
256 } else if (nextTag.isStart("features")) {
257 processStreamFeatures(nextTag);
258 } else if (nextTag.isStart("proceed")) {
259 switchOverToTls(nextTag);
260 } else if (nextTag.isStart("success")) {
261 final String challenge = tagReader.readElement(nextTag).getContent();
262 try {
263 saslMechanism.getResponse(challenge);
264 } catch (final SaslMechanism.AuthenticationException e) {
265 disconnect(true);
266 Log.e(Config.LOGTAG, String.valueOf(e));
267 }
268 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": logged in");
269 account.setKey(Account.PINNED_MECHANISM_KEY,
270 String.valueOf(saslMechanism.getPriority()));
271 tagReader.reset();
272 sendStartStream();
273 processStream(tagReader.readTag());
274 break;
275 } else if (nextTag.isStart("failure")) {
276 tagReader.readElement(nextTag);
277 changeStatus(Account.State.UNAUTHORIZED);
278 } else if (nextTag.isStart("challenge")) {
279 final String challenge = tagReader.readElement(nextTag).getContent();
280 final Element response = new Element("response");
281 response.setAttribute("xmlns",
282 "urn:ietf:params:xml:ns:xmpp-sasl");
283 try {
284 response.setContent(saslMechanism.getResponse(challenge));
285 } catch (final SaslMechanism.AuthenticationException e) {
286 // TODO: Send auth abort tag.
287 Log.e(Config.LOGTAG, e.toString());
288 }
289 tagWriter.writeElement(response);
290 } else if (nextTag.isStart("enabled")) {
291 Element enabled = tagReader.readElement(nextTag);
292 if ("true".equals(enabled.getAttribute("resume"))) {
293 this.streamId = enabled.getAttribute("id");
294 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
295 + ": stream managment(" + smVersion
296 + ") enabled (resumable)");
297 } else {
298 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
299 + ": stream managment(" + smVersion + ") enabled");
300 }
301 this.lastSessionStarted = SystemClock.elapsedRealtime();
302 this.stanzasReceived = 0;
303 RequestPacket r = new RequestPacket(smVersion);
304 tagWriter.writeStanzaAsync(r);
305 } else if (nextTag.isStart("resumed")) {
306 lastPaketReceived = SystemClock.elapsedRealtime();
307 Element resumed = tagReader.readElement(nextTag);
308 String h = resumed.getAttribute("h");
309 try {
310 int serverCount = Integer.parseInt(h);
311 if (serverCount != stanzasSent) {
312 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
313 + ": session resumed with lost packages");
314 stanzasSent = serverCount;
315 } else {
316 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
317 + ": session resumed");
318 }
319 if (acknowledgedListener != null) {
320 for (int i = 0; i < messageReceipts.size(); ++i) {
321 if (serverCount >= messageReceipts.keyAt(i)) {
322 acknowledgedListener.onMessageAcknowledged(
323 account, messageReceipts.valueAt(i));
324 }
325 }
326 }
327 messageReceipts.clear();
328 } catch (final NumberFormatException ignored) {
329
330 }
331 sendServiceDiscoveryInfo(account.getServer());
332 sendServiceDiscoveryItems(account.getServer());
333 sendInitialPing();
334 } else if (nextTag.isStart("r")) {
335 tagReader.readElement(nextTag);
336 AckPacket ack = new AckPacket(this.stanzasReceived, smVersion);
337 tagWriter.writeStanzaAsync(ack);
338 } else if (nextTag.isStart("a")) {
339 Element ack = tagReader.readElement(nextTag);
340 lastPaketReceived = SystemClock.elapsedRealtime();
341 int serverSequence = Integer.parseInt(ack.getAttribute("h"));
342 String msgId = this.messageReceipts.get(serverSequence);
343 if (msgId != null) {
344 if (this.acknowledgedListener != null) {
345 this.acknowledgedListener.onMessageAcknowledged(
346 account, msgId);
347 }
348 this.messageReceipts.remove(serverSequence);
349 }
350 } else if (nextTag.isStart("failed")) {
351 tagReader.readElement(nextTag);
352 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": resumption failed");
353 streamId = null;
354 if (account.getStatus() != Account.State.ONLINE) {
355 sendBindRequest();
356 }
357 } else if (nextTag.isStart("iq")) {
358 processIq(nextTag);
359 } else if (nextTag.isStart("message")) {
360 processMessage(nextTag);
361 } else if (nextTag.isStart("presence")) {
362 processPresence(nextTag);
363 }
364 nextTag = tagReader.readTag();
365 }
366 if (account.getStatus() == Account.State.ONLINE) {
367 account. setStatus(Account.State.OFFLINE);
368 if (statusListener != null) {
369 statusListener.onStatusChanged(account);
370 }
371 }
372 }
373
374 private void sendInitialPing() {
375 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": sending intial ping");
376 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
377 iq.setFrom(account.getJid());
378 iq.addChild("ping", "urn:xmpp:ping");
379 this.sendIqPacket(iq, new OnIqPacketReceived() {
380
381 @Override
382 public void onIqPacketReceived(Account account, IqPacket packet) {
383 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
384 + ": online with resource " + account.getResource());
385 changeStatus(Account.State.ONLINE);
386 }
387 });
388 }
389
390 private Element processPacket(Tag currentTag, int packetType)
391 throws XmlPullParserException, IOException {
392 Element element;
393 switch (packetType) {
394 case PACKET_IQ:
395 element = new IqPacket();
396 break;
397 case PACKET_MESSAGE:
398 element = new MessagePacket();
399 break;
400 case PACKET_PRESENCE:
401 element = new PresencePacket();
402 break;
403 default:
404 return null;
405 }
406 element.setAttributes(currentTag.getAttributes());
407 Tag nextTag = tagReader.readTag();
408 if (nextTag == null) {
409 throw new IOException("interrupted mid tag");
410 }
411 while (!nextTag.isEnd(element.getName())) {
412 if (!nextTag.isNo()) {
413 Element child = tagReader.readElement(nextTag);
414 String type = currentTag.getAttribute("type");
415 if (packetType == PACKET_IQ
416 && "jingle".equals(child.getName())
417 && ("set".equalsIgnoreCase(type) || "get"
418 .equalsIgnoreCase(type))) {
419 element = new JinglePacket();
420 element.setAttributes(currentTag.getAttributes());
421 }
422 element.addChild(child);
423 }
424 nextTag = tagReader.readTag();
425 if (nextTag == null) {
426 throw new IOException("interrupted mid tag");
427 }
428 }
429 ++stanzasReceived;
430 lastPaketReceived = SystemClock.elapsedRealtime();
431 return element;
432 }
433
434 private void processIq(Tag currentTag) throws XmlPullParserException,
435 IOException {
436 IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
437
438 if (packet.getId() == null) {
439 return; // an iq packet without id is definitely invalid
440 }
441
442 if (packet instanceof JinglePacket) {
443 if (this.jingleListener != null) {
444 this.jingleListener.onJinglePacketReceived(account,
445 (JinglePacket) packet);
446 }
447 } else {
448 if (packetCallbacks.containsKey(packet.getId())) {
449 if (packetCallbacks.get(packet.getId()) instanceof OnIqPacketReceived) {
450 ((OnIqPacketReceived) packetCallbacks.get(packet.getId()))
451 .onIqPacketReceived(account, packet);
452 }
453
454 packetCallbacks.remove(packet.getId());
455 } else if ((packet.getType() == IqPacket.TYPE_GET || packet
456 .getType() == IqPacket.TYPE_SET)
457 && this.unregisteredIqListener != null) {
458 this.unregisteredIqListener.onIqPacketReceived(account, packet);
459 }
460 }
461 }
462
463 private void processMessage(Tag currentTag) throws XmlPullParserException,
464 IOException {
465 MessagePacket packet = (MessagePacket) processPacket(currentTag,
466 PACKET_MESSAGE);
467 String id = packet.getAttribute("id");
468 if ((id != null) && (packetCallbacks.containsKey(id))) {
469 if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) {
470 ((OnMessagePacketReceived) packetCallbacks.get(id))
471 .onMessagePacketReceived(account, packet);
472 }
473 packetCallbacks.remove(id);
474 } else if (this.messageListener != null) {
475 this.messageListener.onMessagePacketReceived(account, packet);
476 }
477 }
478
479 private void processPresence(Tag currentTag) throws XmlPullParserException,
480 IOException {
481 PresencePacket packet = (PresencePacket) processPacket(currentTag,
482 PACKET_PRESENCE);
483 String id = packet.getAttribute("id");
484 if ((id != null) && (packetCallbacks.containsKey(id))) {
485 if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) {
486 ((OnPresencePacketReceived) packetCallbacks.get(id))
487 .onPresencePacketReceived(account, packet);
488 }
489 packetCallbacks.remove(id);
490 } else if (this.presenceListener != null) {
491 this.presenceListener.onPresencePacketReceived(account, packet);
492 }
493 }
494
495 private void sendStartTLS() throws IOException {
496 Tag startTLS = Tag.empty("starttls");
497 startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
498 tagWriter.writeTag(startTLS);
499 }
500
501 private SharedPreferences getPreferences() {
502 return PreferenceManager
503 .getDefaultSharedPreferences(applicationContext);
504 }
505
506 private boolean enableLegacySSL() {
507 return getPreferences().getBoolean("enable_legacy_ssl", false);
508 }
509
510 private void switchOverToTls(final Tag currentTag) throws XmlPullParserException,
511 IOException {
512 tagReader.readTag();
513 try {
514 SSLContext sc = SSLContext.getInstance("TLS");
515 sc.init(null,
516 new X509TrustManager[]{this.mXmppConnectionService.getMemorizingTrustManager()},
517 mXmppConnectionService.getRNG());
518 SSLSocketFactory factory = sc.getSocketFactory();
519
520 if (factory == null) {
521 throw new IOException("SSLSocketFactory was null");
522 }
523
524 final HostnameVerifier verifier = this.mXmppConnectionService.getMemorizingTrustManager().wrapHostnameVerifier(new StrictHostnameVerifier());
525
526 if (socket == null || socket.isClosed()) {
527 throw new IOException("socket null or closed");
528 }
529 final InetAddress address = socket.getInetAddress();
530 if (address == null) {
531 throw new IOException("socket address was null");
532 }
533
534 final SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,address.getHostAddress(), socket.getPort(),true);
535
536 // Support all protocols except legacy SSL.
537 // The min SDK version prevents us having to worry about SSLv2. In
538 // future, this may be true of SSLv3 as well.
539 final String[] supportProtocols;
540 if (enableLegacySSL()) {
541 supportProtocols = sslSocket.getSupportedProtocols();
542 } else {
543 final List<String> supportedProtocols = new LinkedList<>(
544 Arrays.asList(sslSocket.getSupportedProtocols()));
545 supportedProtocols.remove("SSLv3");
546 supportProtocols = new String[supportedProtocols.size()];
547 supportedProtocols.toArray(supportProtocols);
548 }
549 sslSocket.setEnabledProtocols(supportProtocols);
550
551 if (verifier != null
552 && !verifier.verify(account.getServer().getDomainpart(),
553 sslSocket.getSession())) {
554 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": TLS certificate verification failed");
555 disconnect(true);
556 changeStatus(Account.State.SECURITY_ERROR);
557 }
558 tagReader.setInputStream(sslSocket.getInputStream());
559 tagWriter.setOutputStream(sslSocket.getOutputStream());
560 sendStartStream();
561 Log.d(Config.LOGTAG, account.getJid().toBareJid()
562 + ": TLS connection established");
563 enabledEncryption = true;
564 processStream(tagReader.readTag());
565 sslSocket.close();
566 } catch (final NoSuchAlgorithmException | KeyManagementException e1) {
567 e1.printStackTrace();
568 }
569 }
570
571 private void processStreamFeatures(Tag currentTag)
572 throws XmlPullParserException, IOException {
573 this.streamFeatures = tagReader.readElement(currentTag);
574 if (this.streamFeatures.hasChild("starttls") && !enabledEncryption) {
575 sendStartTLS();
576 } else if (this.streamFeatures.hasChild("register")
577 && account.isOptionSet(Account.OPTION_REGISTER)
578 && enabledEncryption) {
579 sendRegistryRequest();
580 } else if (!this.streamFeatures.hasChild("register")
581 && account.isOptionSet(Account.OPTION_REGISTER)) {
582 changeStatus(Account.State.REGISTRATION_NOT_SUPPORTED);
583 disconnect(true);
584 } else if (this.streamFeatures.hasChild("mechanisms")
585 && shouldAuthenticate && enabledEncryption) {
586 final List<String> mechanisms = extractMechanisms(streamFeatures
587 .findChild("mechanisms"));
588 final Element auth = new Element("auth");
589 auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
590 if (mechanisms.contains("SCRAM-SHA-1")) {
591 saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG());
592 } else if (mechanisms.contains("DIGEST-MD5")) {
593 saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG());
594 } else if (mechanisms.contains("PLAIN")) {
595 saslMechanism = new Plain(tagWriter, account);
596 }
597 final JSONObject keys = account.getKeys();
598 try {
599 if (keys.has(Account.PINNED_MECHANISM_KEY) &&
600 keys.getInt(Account.PINNED_MECHANISM_KEY) > saslMechanism.getPriority() ) {
601 Log.e(Config.LOGTAG, "Auth failed. Authentication mechanism " + saslMechanism.getMechanism() +
602 " has lower priority (" + String.valueOf(saslMechanism.getPriority()) +
603 ") than pinned priority (" + keys.getInt(Account.PINNED_MECHANISM_KEY) +
604 "). Possible downgrade attack?");
605 disconnect(true);
606 changeStatus(Account.State.SECURITY_ERROR);
607 }
608 } catch (final JSONException e) {
609 Log.d(Config.LOGTAG, "Parse error while checking pinned auth mechanism");
610 }
611 Log.d(Config.LOGTAG,account.getJid().toString()+": Authenticating with " + saslMechanism.getMechanism());
612 auth.setAttribute("mechanism", saslMechanism.getMechanism());
613 if (!saslMechanism.getClientFirstMessage().isEmpty()) {
614 auth.setContent(saslMechanism.getClientFirstMessage());
615 }
616 tagWriter.writeElement(auth);
617 } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:"
618 + smVersion)
619 && streamId != null) {
620 ResumePacket resume = new ResumePacket(this.streamId,
621 stanzasReceived, smVersion);
622 this.tagWriter.writeStanzaAsync(resume);
623 } else if (this.streamFeatures.hasChild("bind") && shouldBind) {
624 sendBindRequest();
625 } else {
626 disconnect(true);
627 changeStatus(Account.State.INCOMPATIBLE_SERVER);
628 }
629 }
630
631 private List<String> extractMechanisms(Element stream) {
632 ArrayList<String> mechanisms = new ArrayList<>(stream
633 .getChildren().size());
634 for (Element child : stream.getChildren()) {
635 mechanisms.add(child.getContent());
636 }
637 return mechanisms;
638 }
639
640 private void sendRegistryRequest() {
641 IqPacket register = new IqPacket(IqPacket.TYPE_GET);
642 register.query("jabber:iq:register");
643 register.setTo(account.getServer());
644 sendIqPacket(register, new OnIqPacketReceived() {
645
646 @Override
647 public void onIqPacketReceived(Account account, IqPacket packet) {
648 Element instructions = packet.query().findChild("instructions");
649 if (packet.query().hasChild("username")
650 && (packet.query().hasChild("password"))) {
651 IqPacket register = new IqPacket(IqPacket.TYPE_SET);
652 Element username = new Element("username")
653 .setContent(account.getUsername());
654 Element password = new Element("password")
655 .setContent(account.getPassword());
656 register.query("jabber:iq:register").addChild(username);
657 register.query().addChild(password);
658 sendIqPacket(register, new OnIqPacketReceived() {
659
660 @Override
661 public void onIqPacketReceived(Account account,
662 IqPacket packet) {
663 if (packet.getType() == IqPacket.TYPE_RESULT) {
664 account.setOption(Account.OPTION_REGISTER,
665 false);
666 changeStatus(Account.State.REGISTRATION_SUCCESSFUL);
667 } else if (packet.hasChild("error")
668 && (packet.findChild("error")
669 .hasChild("conflict"))) {
670 changeStatus(Account.State.REGISTRATION_CONFLICT);
671 } else {
672 changeStatus(Account.State.REGISTRATION_FAILED);
673 Log.d(Config.LOGTAG, packet.toString());
674 }
675 disconnect(true);
676 }
677 });
678 } else {
679 changeStatus(Account.State.REGISTRATION_FAILED);
680 disconnect(true);
681 Log.d(Config.LOGTAG, account.getJid().toBareJid()
682 + ": could not register. instructions are"
683 + instructions.getContent());
684 }
685 }
686 });
687 }
688
689 private void sendBindRequest() throws IOException {
690 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
691 iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind")
692 .addChild("resource").setContent(account.getResource());
693 this.sendUnboundIqPacket(iq, new OnIqPacketReceived() {
694 @Override
695 public void onIqPacketReceived(Account account, IqPacket packet) {
696 Element bind = packet.findChild("bind");
697 if (bind != null) {
698 final Element jid = bind.findChild("jid");
699 if (jid != null && jid.getContent() != null) {
700 try {
701 account.setResource(Jid.fromString(jid.getContent()).getResourcepart());
702 } catch (final InvalidJidException e) {
703 // TODO: Handle the case where an external JID is technically invalid?
704 }
705 if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) {
706 smVersion = 3;
707 EnablePacket enable = new EnablePacket(smVersion);
708 tagWriter.writeStanzaAsync(enable);
709 stanzasSent = 0;
710 messageReceipts.clear();
711 } else if (streamFeatures.hasChild("sm",
712 "urn:xmpp:sm:2")) {
713 smVersion = 2;
714 EnablePacket enable = new EnablePacket(smVersion);
715 tagWriter.writeStanzaAsync(enable);
716 stanzasSent = 0;
717 messageReceipts.clear();
718 }
719 enabledCarbons = false;
720 disco.clear();
721 sendServiceDiscoveryInfo(account.getServer());
722 sendServiceDiscoveryItems(account.getServer());
723 if (bindListener != null) {
724 bindListener.onBind(account);
725 }
726 sendInitialPing();
727 } else {
728 disconnect(true);
729 }
730 } else {
731 disconnect(true);
732 }
733 }
734 });
735 if (this.streamFeatures.hasChild("session")) {
736 Log.d(Config.LOGTAG, account.getJid().toBareJid()
737 + ": sending deprecated session");
738 IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
739 startSession.addChild("session",
740 "urn:ietf:params:xml:ns:xmpp-session");
741 this.sendUnboundIqPacket(startSession, null);
742 }
743 }
744
745 private void sendServiceDiscoveryInfo(final Jid server) {
746 if (disco.containsKey(server.toDomainJid().toString())) {
747 if (account.getServer().equals(server.toDomainJid())) {
748 enableAdvancedStreamFeatures();
749 }
750 } else {
751 final IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
752 iq.setTo(server.toDomainJid());
753 iq.query("http://jabber.org/protocol/disco#info");
754 this.sendIqPacket(iq, new OnIqPacketReceived() {
755
756 @Override
757 public void onIqPacketReceived(Account account, IqPacket packet) {
758 final List<Element> elements = packet.query().getChildren();
759 final List<String> features = new ArrayList<>();
760 for (Element element : elements) {
761 if (element.getName().equals("identity")) {
762 if ("irc".equals(element.getAttribute("type"))) {
763 //add fake feature to not confuse irc and real muc
764 features.add("siacs:no:muc");
765 }
766 } else if (element.getName().equals("feature")) {
767 features.add(element.getAttribute("var"));
768 }
769 }
770 disco.put(server.toDomainJid().toString(), features);
771
772 if (account.getServer().equals(server.toDomainJid())) {
773 enableAdvancedStreamFeatures();
774 }
775 }
776 });
777 }
778 }
779
780 private void enableAdvancedStreamFeatures() {
781 if (getFeatures().carbons()) {
782 if (!enabledCarbons) {
783 sendEnableCarbons();
784 }
785 }
786 }
787
788 private void sendServiceDiscoveryItems(final Jid server) {
789 final IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
790 iq.setTo(server.toDomainJid());
791 iq.query("http://jabber.org/protocol/disco#items");
792 this.sendIqPacket(iq, new OnIqPacketReceived() {
793
794 @Override
795 public void onIqPacketReceived(Account account, IqPacket packet) {
796 List<Element> elements = packet.query().getChildren();
797 for (Element element : elements) {
798 if (element.getName().equals("item")) {
799 final Jid jid = element.getAttributeAsJid("jid");
800 if (jid != null && !jid.equals(account.getServer())) {
801 sendServiceDiscoveryInfo(jid);
802 }
803 }
804 }
805 }
806 });
807 }
808
809 private void sendEnableCarbons() {
810 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
811 iq.addChild("enable", "urn:xmpp:carbons:2");
812 this.sendIqPacket(iq, new OnIqPacketReceived() {
813
814 @Override
815 public void onIqPacketReceived(Account account, IqPacket packet) {
816 if (!packet.hasChild("error")) {
817 Log.d(Config.LOGTAG, account.getJid().toBareJid()
818 + ": successfully enabled carbons");
819 enabledCarbons = true;
820 } else {
821 Log.d(Config.LOGTAG, account.getJid().toBareJid()
822 + ": error enableing carbons " + packet.toString());
823 }
824 }
825 });
826 }
827
828 private void processStreamError(Tag currentTag)
829 throws XmlPullParserException, IOException {
830 Element streamError = tagReader.readElement(currentTag);
831 if (streamError != null && streamError.hasChild("conflict")) {
832 final String resource = account.getResource().split("\\.")[0];
833 account.setResource(resource + "." + nextRandomId());
834 Log.d(Config.LOGTAG,
835 account.getJid().toBareJid() + ": switching resource due to conflict ("
836 + account.getResource() + ")");
837 }
838 }
839
840 private void sendStartStream() throws IOException {
841 Tag stream = Tag.start("stream:stream");
842 stream.setAttribute("from", account.getJid().toBareJid().toString());
843 stream.setAttribute("to", account.getServer().toString());
844 stream.setAttribute("version", "1.0");
845 stream.setAttribute("xml:lang", "en");
846 stream.setAttribute("xmlns", "jabber:client");
847 stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
848 tagWriter.writeTag(stream);
849 }
850
851 private String nextRandomId() {
852 return new BigInteger(50, mXmppConnectionService.getRNG()).toString(32);
853 }
854
855 public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
856 if (packet.getId() == null) {
857 String id = nextRandomId();
858 packet.setAttribute("id", id);
859 }
860 packet.setFrom(account.getJid());
861 this.sendPacket(packet, callback);
862 }
863
864 public void sendUnboundIqPacket(IqPacket packet, OnIqPacketReceived callback) {
865 if (packet.getId() == null) {
866 String id = nextRandomId();
867 packet.setAttribute("id", id);
868 }
869 this.sendPacket(packet, callback);
870 }
871
872 public void sendMessagePacket(MessagePacket packet) {
873 this.sendPacket(packet, null);
874 }
875
876 public void sendPresencePacket(PresencePacket packet) {
877 this.sendPacket(packet, null);
878 }
879
880 private synchronized void sendPacket(final AbstractStanza packet,
881 PacketReceived callback) {
882 if (packet.getName().equals("iq") || packet.getName().equals("message")
883 || packet.getName().equals("presence")) {
884 ++stanzasSent;
885 }
886 tagWriter.writeStanzaAsync(packet);
887 if (packet instanceof MessagePacket && packet.getId() != null
888 && this.streamId != null) {
889 Log.d(Config.LOGTAG, "request delivery report for stanza "
890 + stanzasSent);
891 this.messageReceipts.put(stanzasSent, packet.getId());
892 tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion));
893 }
894 if (callback != null) {
895 if (packet.getId() == null) {
896 packet.setId(nextRandomId());
897 }
898 packetCallbacks.put(packet.getId(), callback);
899 }
900 }
901
902 public void sendPing() {
903 if (streamFeatures.hasChild("sm")) {
904 tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
905 } else {
906 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
907 iq.setFrom(account.getJid());
908 iq.addChild("ping", "urn:xmpp:ping");
909 this.sendIqPacket(iq, null);
910 }
911 this.lastPingSent = SystemClock.elapsedRealtime();
912 }
913
914 public void setOnMessagePacketReceivedListener(
915 OnMessagePacketReceived listener) {
916 this.messageListener = listener;
917 }
918
919 public void setOnUnregisteredIqPacketReceivedListener(
920 OnIqPacketReceived listener) {
921 this.unregisteredIqListener = listener;
922 }
923
924 public void setOnPresencePacketReceivedListener(
925 OnPresencePacketReceived listener) {
926 this.presenceListener = listener;
927 }
928
929 public void setOnJinglePacketReceivedListener(
930 OnJinglePacketReceived listener) {
931 this.jingleListener = listener;
932 }
933
934 public void setOnStatusChangedListener(OnStatusChanged listener) {
935 this.statusListener = listener;
936 }
937
938 public void setOnBindListener(OnBindListener listener) {
939 this.bindListener = listener;
940 }
941
942 public void setOnMessageAcknowledgeListener(OnMessageAcknowledged listener) {
943 this.acknowledgedListener = listener;
944 }
945
946 public void disconnect(boolean force) {
947 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": disconnecting");
948 try {
949 if (force) {
950 socket.close();
951 return;
952 }
953 new Thread(new Runnable() {
954
955 @Override
956 public void run() {
957 if (tagWriter.isActive()) {
958 tagWriter.finish();
959 try {
960 while (!tagWriter.finished()) {
961 Log.d(Config.LOGTAG, "not yet finished");
962 Thread.sleep(100);
963 }
964 tagWriter.writeTag(Tag.end("stream:stream"));
965 socket.close();
966 } catch (IOException e) {
967 Log.d(Config.LOGTAG,
968 "io exception during disconnect");
969 } catch (InterruptedException e) {
970 Log.d(Config.LOGTAG, "interrupted");
971 }
972 }
973 }
974 }).start();
975 } catch (IOException e) {
976 Log.d(Config.LOGTAG, "io exception during disconnect");
977 }
978 }
979
980 public List<String> findDiscoItemsByFeature(String feature) {
981 final List<String> items = new ArrayList<>();
982 for (Entry<String, List<String>> cursor : disco.entrySet()) {
983 if (cursor.getValue().contains(feature)) {
984 items.add(cursor.getKey());
985 }
986 }
987 return items;
988 }
989
990 public String findDiscoItemByFeature(String feature) {
991 List<String> items = findDiscoItemsByFeature(feature);
992 if (items.size() >= 1) {
993 return items.get(0);
994 }
995 return null;
996 }
997
998 public void r() {
999 this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
1000 }
1001
1002 public String getMucServer() {
1003 final List<String> items = new ArrayList<>();
1004 for (Entry<String, List<String>> cursor : disco.entrySet()) {
1005 final List<String> value = cursor.getValue();
1006 if (value.contains("http://jabber.org/protocol/muc") && !value.contains("jabber:iq:gateway") && !value.contains("siacs:no:muc")) {
1007 return cursor.getKey();
1008 }
1009 }
1010 return null;
1011 }
1012
1013 public int getTimeToNextAttempt() {
1014 int interval = (int) (25 * Math.pow(1.5, attempt));
1015 int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
1016 return interval - secondsSinceLast;
1017 }
1018
1019 public int getAttempt() {
1020 return this.attempt;
1021 }
1022
1023 public Features getFeatures() {
1024 return this.features;
1025 }
1026
1027 public long getLastSessionEstablished() {
1028 long diff;
1029 if (this.lastSessionStarted == 0) {
1030 diff = SystemClock.elapsedRealtime() - this.lastConnect;
1031 } else {
1032 diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
1033 }
1034 return System.currentTimeMillis() - diff;
1035 }
1036
1037 public long getLastConnect() {
1038 return this.lastConnect;
1039 }
1040
1041 public long getLastPingSent() {
1042 return this.lastPingSent;
1043 }
1044
1045 public long getLastPacketReceived() {
1046 return this.lastPaketReceived;
1047 }
1048
1049 public void sendActive() {
1050 this.sendPacket(new ActivePacket(), null);
1051 }
1052
1053 public void sendInactive() {
1054 this.sendPacket(new InactivePacket(), null);
1055 }
1056
1057 public class Features {
1058 XmppConnection connection;
1059
1060 public Features(XmppConnection connection) {
1061 this.connection = connection;
1062 }
1063
1064 private boolean hasDiscoFeature(final Jid server, final String feature) {
1065 return connection.disco.containsKey(server.toDomainJid().toString()) &&
1066 connection.disco.get(server.toDomainJid().toString()).contains(feature);
1067 }
1068
1069 public boolean carbons() {
1070 return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2");
1071 }
1072
1073 public boolean sm() {
1074 return streamId != null;
1075 }
1076
1077 public boolean csi() {
1078 return connection.streamFeatures != null && connection.streamFeatures.hasChild("csi", "urn:xmpp:csi:0");
1079 }
1080
1081 public boolean pubsub() {
1082 return hasDiscoFeature(account.getServer(),
1083 "http://jabber.org/protocol/pubsub#publish");
1084 }
1085
1086 public boolean mam() {
1087 return hasDiscoFeature(account.getServer(), "urn:xmpp:mam:0");
1088 }
1089
1090 public boolean rosterVersioning() {
1091 return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver");
1092 }
1093
1094 public boolean streamhost() {
1095 return connection
1096 .findDiscoItemByFeature("http://jabber.org/protocol/bytestreams") != null;
1097 }
1098 }
1099}