diff options
Diffstat (limited to 'src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java')
-rw-r--r-- | src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java | 825 |
1 files changed, 429 insertions, 396 deletions
diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 2b9d6632..9148aa72 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -12,12 +12,15 @@ import android.util.Log; import android.util.SparseArray; import org.apache.http.conn.ssl.StrictHostnameVerifier; +import org.json.JSONException; +import org.json.JSONObject; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.math.BigInteger; +import java.net.IDN; import java.net.InetSocketAddress; import java.net.Socket; import java.net.UnknownHostException; @@ -38,9 +41,12 @@ import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509TrustManager; import eu.siacs.conversations.Config; +import eu.siacs.conversations.crypto.sasl.DigestMd5; +import eu.siacs.conversations.crypto.sasl.Plain; +import eu.siacs.conversations.crypto.sasl.SaslMechanism; +import eu.siacs.conversations.crypto.sasl.ScramSha1; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.DNSHelper; import eu.siacs.conversations.utils.zlib.ZLibInputStream; import eu.siacs.conversations.utils.zlib.ZLibOutputStream; @@ -48,6 +54,8 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Tag; import eu.siacs.conversations.xml.TagWriter; import eu.siacs.conversations.xml.XmlReader; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; @@ -76,12 +84,16 @@ public class XmppConnection implements Runnable { private boolean shouldBind = true; private boolean shouldAuthenticate = true; private Element streamFeatures; - private HashMap<String, List<String>> disco = new HashMap<String, List<String>>(); + private HashMap<String, List<String>> disco = new HashMap<>(); + private String streamId = null; private int smVersion = 3; - private SparseArray<String> messageReceipts = new SparseArray<String>(); - private boolean usingCompression = false; - private boolean usingEncryption = false; + private SparseArray<String> messageReceipts = new SparseArray<>(); + + private boolean enabledCompression = false; + private boolean enabledEncryption = false; + private boolean enabledCarbons = false; + private int stanzasReceived = 0; private int stanzasSent = 0; private long lastPaketReceived = 0; @@ -89,7 +101,7 @@ public class XmppConnection implements Runnable { private long lastConnect = 0; private long lastSessionStarted = 0; private int attempt = 0; - private Hashtable<String, PacketReceived> packetCallbacks = new Hashtable<String, PacketReceived>(); + private Hashtable<String, PacketReceived> packetCallbacks = new Hashtable<>(); private OnPresencePacketReceived presenceListener = null; private OnJinglePacketReceived jingleListener = null; private OnIqPacketReceived unregisteredIqListener = null; @@ -99,24 +111,26 @@ public class XmppConnection implements Runnable { private OnMessageAcknowledged acknowledgedListener = null; private XmppConnectionService mXmppConnectionService = null; + private SaslMechanism saslMechanism; + public XmppConnection(Account account, XmppConnectionService service) { this.account = account; this.wakeLock = service.getPowerManager().newWakeLock( - PowerManager.PARTIAL_WAKE_LOCK, account.getJid()); + PowerManager.PARTIAL_WAKE_LOCK, account.getJid().toBareJid().toString()); tagWriter = new TagWriter(); mXmppConnectionService = service; applicationContext = service.getApplicationContext(); } - protected void changeStatus(int nextStatus) { + protected void changeStatus(final Account.State nextStatus) { if (account.getStatus() != nextStatus) { - if ((nextStatus == Account.STATUS_OFFLINE) - && (account.getStatus() != Account.STATUS_CONNECTING) - && (account.getStatus() != Account.STATUS_ONLINE) - && (account.getStatus() != Account.STATUS_DISABLED)) { + if ((nextStatus == Account.State.OFFLINE) + && (account.getStatus() != Account.State.CONNECTING) + && (account.getStatus() != Account.State.ONLINE) + && (account.getStatus() != Account.State.DISABLED)) { return; } - if (nextStatus == Account.STATUS_ONLINE) { + if (nextStatus == Account.State.ONLINE) { this.attempt = 0; } account.setStatus(nextStatus); @@ -127,24 +141,24 @@ public class XmppConnection implements Runnable { } protected void connect() { - Log.d(Config.LOGTAG, account.getJid() + ": connecting"); - usingCompression = false; - usingEncryption = false; + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": connecting"); + enabledCompression = false; + enabledEncryption = false; lastConnect = SystemClock.elapsedRealtime(); lastPingSent = SystemClock.elapsedRealtime(); this.attempt++; try { shouldAuthenticate = shouldBind = !account - .isOptionSet(Account.OPTION_REGISTER); + .isOptionSet(Account.OPTION_REGISTER); tagReader = new XmlReader(wakeLock); tagWriter = new TagWriter(); packetCallbacks.clear(); - this.changeStatus(Account.STATUS_CONNECTING); + this.changeStatus(Account.State.CONNECTING); Bundle result = DNSHelper.getSRVRecord(account.getServer()); ArrayList<Parcelable> values = result.getParcelableArrayList("values"); if ("timeout".equals(result.getString("error"))) { - Log.d(Config.LOGTAG, account.getJid() + ": dns timeout"); - this.changeStatus(Account.STATUS_OFFLINE); + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": dns timeout"); + this.changeStatus(Account.State.OFFLINE); return; } else if (values != null) { int i = 0; @@ -152,18 +166,24 @@ public class XmppConnection implements Runnable { while (socketError && values.size() > i) { Bundle namePort = (Bundle) values.get(i); try { - String srvRecordServer = namePort.getString("name"); + String srvRecordServer; + try { + srvRecordServer=IDN.toASCII(namePort.getString("name")); + } catch (final IllegalArgumentException e) { + // TODO: Handle me?` + srvRecordServer = ""; + } int srvRecordPort = namePort.getInt("port"); String srvIpServer = namePort.getString("ipv4"); InetSocketAddress addr; if (srvIpServer != null) { addr = new InetSocketAddress(srvIpServer, srvRecordPort); - Log.d(Config.LOGTAG, account.getJid() + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": using values from dns " + srvRecordServer + "[" + srvIpServer + "]:" + srvRecordPort); } else { addr = new InetSocketAddress(srvRecordServer, srvRecordPort); - Log.d(Config.LOGTAG, account.getJid() + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": using values from dns " + srvRecordServer + ":" + srvRecordPort); } @@ -171,30 +191,30 @@ public class XmppConnection implements Runnable { socket.connect(addr, 20000); socketError = false; } catch (UnknownHostException e) { - Log.d(Config.LOGTAG, account.getJid() + ": " + e.getMessage()); + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); i++; } catch (IOException e) { - Log.d(Config.LOGTAG, account.getJid() + ": " + e.getMessage()); + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); i++; } } if (socketError) { - this.changeStatus(Account.STATUS_SERVER_NOT_FOUND); + this.changeStatus(Account.State.SERVER_NOT_FOUND); if (wakeLock.isHeld()) { try { wakeLock.release(); - } catch (RuntimeException re) { + } catch (final RuntimeException ignored) { } } return; } } else if (result.containsKey("error") && "nosrv".equals(result.getString("error", null))) { - socket = new Socket(account.getServer(), 5222); + socket = new Socket(account.getServer().getDomainpart(), 5222); } else { - Log.d(Config.LOGTAG, account.getJid() + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": timeout in DNS resolution"); - changeStatus(Account.STATUS_OFFLINE); + changeStatus(Account.State.OFFLINE); return; } OutputStream out = socket.getOutputStream(); @@ -218,45 +238,32 @@ public class XmppConnection implements Runnable { socket.close(); } } catch (UnknownHostException e) { - this.changeStatus(Account.STATUS_SERVER_NOT_FOUND); + this.changeStatus(Account.State.SERVER_NOT_FOUND); if (wakeLock.isHeld()) { try { wakeLock.release(); - } catch (RuntimeException re) { + } catch (final RuntimeException ignored) { } } - return; - } catch (IOException e) { - Log.d(Config.LOGTAG, account.getJid() + ": " + e.getMessage()); - this.changeStatus(Account.STATUS_OFFLINE); + } catch (final IOException | XmlPullParserException e) { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); + this.changeStatus(Account.State.OFFLINE); if (wakeLock.isHeld()) { try { wakeLock.release(); - } catch (RuntimeException re) { + } catch (final RuntimeException ignored) { } } - return; } catch (NoSuchAlgorithmException e) { - Log.d(Config.LOGTAG, account.getJid() + ": " + e.getMessage()); - this.changeStatus(Account.STATUS_OFFLINE); + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); + this.changeStatus(Account.State.OFFLINE); Log.d(Config.LOGTAG, "compression exception " + e.getMessage()); if (wakeLock.isHeld()) { try { wakeLock.release(); - } catch (RuntimeException re) { + } catch (final RuntimeException ignored) { } } - return; - } catch (XmlPullParserException e) { - Log.d(Config.LOGTAG, account.getJid() + ": " + e.getMessage()); - this.changeStatus(Account.STATUS_OFFLINE); - if (wakeLock.isHeld()) { - try { - wakeLock.release(); - } catch (RuntimeException re) { - } - } - return; } } @@ -266,137 +273,151 @@ public class XmppConnection implements Runnable { connect(); } - private void processStream(Tag currentTag) throws XmlPullParserException, - IOException, NoSuchAlgorithmException { - Tag nextTag = tagReader.readTag(); - while ((nextTag != null) && (!nextTag.isEnd("stream"))) { - if (nextTag.isStart("error")) { - processStreamError(nextTag); - } else if (nextTag.isStart("features")) { - processStreamFeatures(nextTag); - } else if (nextTag.isStart("proceed")) { - switchOverToTls(nextTag); - } else if (nextTag.isStart("compressed")) { - switchOverToZLib(nextTag); - } else if (nextTag.isStart("success")) { - Log.d(Config.LOGTAG, account.getJid() + ": logged in"); - tagReader.readTag(); - tagReader.reset(); - sendStartStream(); - processStream(tagReader.readTag()); - break; - } else if (nextTag.isStart("failure")) { - tagReader.readElement(nextTag); - changeStatus(Account.STATUS_UNAUTHORIZED); - } else if (nextTag.isStart("challenge")) { - String challange = tagReader.readElement(nextTag).getContent(); - Element response = new Element("response"); - response.setAttribute("xmlns", - "urn:ietf:params:xml:ns:xmpp-sasl"); - response.setContent(CryptoHelper.saslDigestMd5(account, - challange, mXmppConnectionService.getRNG())); - tagWriter.writeElement(response); - } else if (nextTag.isStart("enabled")) { - Element enabled = tagReader.readElement(nextTag); - if ("true".equals(enabled.getAttribute("resume"))) { - this.streamId = enabled.getAttribute("id"); - Log.d(Config.LOGTAG, account.getJid() - + ": stream managment(" + smVersion - + ") enabled (resumable)"); - } else { - Log.d(Config.LOGTAG, account.getJid() - + ": stream managment(" + smVersion + ") enabled"); - } - this.lastSessionStarted = SystemClock.elapsedRealtime(); - this.stanzasReceived = 0; - RequestPacket r = new RequestPacket(smVersion); - tagWriter.writeStanzaAsync(r); - } else if (nextTag.isStart("resumed")) { - lastPaketReceived = SystemClock.elapsedRealtime(); - Element resumed = tagReader.readElement(nextTag); - String h = resumed.getAttribute("h"); - try { - int serverCount = Integer.parseInt(h); - if (serverCount != stanzasSent) { - Log.d(Config.LOGTAG, account.getJid() - + ": session resumed with lost packages"); - stanzasSent = serverCount; - } else { - Log.d(Config.LOGTAG, account.getJid() - + ": session resumed"); - } - if (acknowledgedListener != null) { - for (int i = 0; i < messageReceipts.size(); ++i) { - if (serverCount >= messageReceipts.keyAt(i)) { - acknowledgedListener.onMessageAcknowledged( - account, messageReceipts.valueAt(i)); + private void processStream(final Tag currentTag) throws XmlPullParserException, + IOException, NoSuchAlgorithmException { + Tag nextTag = tagReader.readTag(); + + while ((nextTag != null) && (!nextTag.isEnd("stream"))) { + if (nextTag.isStart("error")) { + processStreamError(nextTag); + } else if (nextTag.isStart("features")) { + processStreamFeatures(nextTag); + } else if (nextTag.isStart("proceed")) { + switchOverToTls(nextTag); + } else if (nextTag.isStart("compressed")) { + switchOverToZLib(nextTag); + } else if (nextTag.isStart("success")) { + final String challenge = tagReader.readElement(nextTag).getContent(); + try { + saslMechanism.getResponse(challenge); + } catch (final SaslMechanism.AuthenticationException e) { + disconnect(true); + Log.e(Config.LOGTAG, String.valueOf(e)); + } + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": logged in"); + account.setKey(Account.PINNED_MECHANISM_KEY, + String.valueOf(saslMechanism.getPriority())); + tagReader.reset(); + sendStartStream(); + processStream(tagReader.readTag()); + break; + } else if (nextTag.isStart("failure")) { + tagReader.readElement(nextTag); + changeStatus(Account.State.UNAUTHORIZED); + } else if (nextTag.isStart("challenge")) { + final String challenge = tagReader.readElement(nextTag).getContent(); + final Element response = new Element("response"); + response.setAttribute("xmlns", + "urn:ietf:params:xml:ns:xmpp-sasl"); + try { + response.setContent(saslMechanism.getResponse(challenge)); + } catch (final SaslMechanism.AuthenticationException e) { + // TODO: Send auth abort tag. + Log.e(Config.LOGTAG, e.toString()); + } + tagWriter.writeElement(response); + } else if (nextTag.isStart("enabled")) { + Element enabled = tagReader.readElement(nextTag); + if ("true".equals(enabled.getAttribute("resume"))) { + this.streamId = enabled.getAttribute("id"); + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + + ": stream managment(" + smVersion + + ") enabled (resumable)"); + } else { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + + ": stream managment(" + smVersion + ") enabled"); + } + this.lastSessionStarted = SystemClock.elapsedRealtime(); + this.stanzasReceived = 0; + RequestPacket r = new RequestPacket(smVersion); + tagWriter.writeStanzaAsync(r); + } else if (nextTag.isStart("resumed")) { + lastPaketReceived = SystemClock.elapsedRealtime(); + Element resumed = tagReader.readElement(nextTag); + String h = resumed.getAttribute("h"); + try { + int serverCount = Integer.parseInt(h); + if (serverCount != stanzasSent) { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + + ": session resumed with lost packages"); + stanzasSent = serverCount; + } else { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + + ": session resumed"); + } + if (acknowledgedListener != null) { + for (int i = 0; i < messageReceipts.size(); ++i) { + if (serverCount >= messageReceipts.keyAt(i)) { + acknowledgedListener.onMessageAcknowledged( + account, messageReceipts.valueAt(i)); + } + } + } + messageReceipts.clear(); + } catch (final NumberFormatException ignored) { + + } + sendServiceDiscoveryInfo(account.getServer()); + sendServiceDiscoveryItems(account.getServer()); + sendInitialPing(); + } else if (nextTag.isStart("r")) { + tagReader.readElement(nextTag); + AckPacket ack = new AckPacket(this.stanzasReceived, smVersion); + tagWriter.writeStanzaAsync(ack); + } else if (nextTag.isStart("a")) { + Element ack = tagReader.readElement(nextTag); + lastPaketReceived = SystemClock.elapsedRealtime(); + int serverSequence = Integer.parseInt(ack.getAttribute("h")); + String msgId = this.messageReceipts.get(serverSequence); + if (msgId != null) { + if (this.acknowledgedListener != null) { + this.acknowledgedListener.onMessageAcknowledged( + account, msgId); + } + this.messageReceipts.remove(serverSequence); + } + } else if (nextTag.isStart("failed")) { + tagReader.readElement(nextTag); + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": resumption failed"); + streamId = null; + if (account.getStatus() != Account.State.ONLINE) { + sendBindRequest(); + } + } else if (nextTag.isStart("iq")) { + processIq(nextTag); + } else if (nextTag.isStart("message")) { + processMessage(nextTag); + } else if (nextTag.isStart("presence")) { + processPresence(nextTag); + } + nextTag = tagReader.readTag(); + } + if (account.getStatus() == Account.State.ONLINE) { + account. setStatus(Account.State.OFFLINE); + if (statusListener != null) { + statusListener.onStatusChanged(account); } } - } - messageReceipts.clear(); - } catch (NumberFormatException e) { - - } - sendInitialPing(); - - } else if (nextTag.isStart("r")) { - tagReader.readElement(nextTag); - AckPacket ack = new AckPacket(this.stanzasReceived, smVersion); - tagWriter.writeStanzaAsync(ack); - } else if (nextTag.isStart("a")) { - Element ack = tagReader.readElement(nextTag); - lastPaketReceived = SystemClock.elapsedRealtime(); - int serverSequence = Integer.parseInt(ack.getAttribute("h")); - String msgId = this.messageReceipts.get(serverSequence); - if (msgId != null) { - if (this.acknowledgedListener != null) { - this.acknowledgedListener.onMessageAcknowledged( - account, msgId); - } - this.messageReceipts.remove(serverSequence); - } - } else if (nextTag.isStart("failed")) { - tagReader.readElement(nextTag); - Log.d(Config.LOGTAG, account.getJid() + ": resumption failed"); - streamId = null; - if (account.getStatus() != Account.STATUS_ONLINE) { - sendBindRequest(); - } - } else if (nextTag.isStart("iq")) { - processIq(nextTag); - } else if (nextTag.isStart("message")) { - processMessage(nextTag); - } else if (nextTag.isStart("presence")) { - processPresence(nextTag); - } - nextTag = tagReader.readTag(); - } - if (account.getStatus() == Account.STATUS_ONLINE) { - account.setStatus(Account.STATUS_OFFLINE); - if (statusListener != null) { - statusListener.onStatusChanged(account); - } - } } private void sendInitialPing() { - Log.d(Config.LOGTAG, account.getJid() + ": sending intial ping"); + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": sending intial ping"); IqPacket iq = new IqPacket(IqPacket.TYPE_GET); - iq.setFrom(account.getFullJid()); + iq.setFrom(account.getJid()); iq.addChild("ping", "urn:xmpp:ping"); this.sendIqPacket(iq, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - Log.d(Config.LOGTAG, account.getJid() + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": online with resource " + account.getResource()); - changeStatus(Account.STATUS_ONLINE); + changeStatus(Account.State.ONLINE); } }); } private Element processPacket(Tag currentTag, int packetType) - throws XmlPullParserException, IOException { + throws XmlPullParserException, IOException { Element element; switch (packetType) { case PACKET_IQ: @@ -423,10 +444,10 @@ public class XmppConnection implements Runnable { if (packetType == PACKET_IQ && "jingle".equals(child.getName()) && ("set".equalsIgnoreCase(type) || "get" - .equalsIgnoreCase(type))) { + .equalsIgnoreCase(type))) { element = new JinglePacket(); element.setAttributes(currentTag.getAttributes()); - } + } element.addChild(child); } nextTag = tagReader.readTag(); @@ -440,64 +461,64 @@ public class XmppConnection implements Runnable { } private void processIq(Tag currentTag) throws XmlPullParserException, - IOException { - IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ); + IOException { + IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ); - if (packet.getId() == null) { - return; // an iq packet without id is definitely invalid - } - - if (packet instanceof JinglePacket) { - if (this.jingleListener != null) { - this.jingleListener.onJinglePacketReceived(account, - (JinglePacket) packet); - } - } else { - if (packetCallbacks.containsKey(packet.getId())) { - if (packetCallbacks.get(packet.getId()) instanceof OnIqPacketReceived) { - ((OnIqPacketReceived) packetCallbacks.get(packet.getId())) - .onIqPacketReceived(account, packet); - } + if (packet.getId() == null) { + return; // an iq packet without id is definitely invalid + } - packetCallbacks.remove(packet.getId()); - } else if ((packet.getType() == IqPacket.TYPE_GET || packet - .getType() == IqPacket.TYPE_SET) - && this.unregisteredIqListener != null) { - this.unregisteredIqListener.onIqPacketReceived(account, packet); - } - } + if (packet instanceof JinglePacket) { + if (this.jingleListener != null) { + this.jingleListener.onJinglePacketReceived(account, + (JinglePacket) packet); + } + } else { + if (packetCallbacks.containsKey(packet.getId())) { + if (packetCallbacks.get(packet.getId()) instanceof OnIqPacketReceived) { + ((OnIqPacketReceived) packetCallbacks.get(packet.getId())) + .onIqPacketReceived(account, packet); + } + + packetCallbacks.remove(packet.getId()); + } else if ((packet.getType() == IqPacket.TYPE_GET || packet + .getType() == IqPacket.TYPE_SET) + && this.unregisteredIqListener != null) { + this.unregisteredIqListener.onIqPacketReceived(account, packet); + } + } } private void processMessage(Tag currentTag) throws XmlPullParserException, - IOException { - MessagePacket packet = (MessagePacket) processPacket(currentTag, - PACKET_MESSAGE); - String id = packet.getAttribute("id"); - if ((id != null) && (packetCallbacks.containsKey(id))) { - if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) { - ((OnMessagePacketReceived) packetCallbacks.get(id)) - .onMessagePacketReceived(account, packet); - } - packetCallbacks.remove(id); - } else if (this.messageListener != null) { - this.messageListener.onMessagePacketReceived(account, packet); - } + IOException { + MessagePacket packet = (MessagePacket) processPacket(currentTag, + PACKET_MESSAGE); + String id = packet.getAttribute("id"); + if ((id != null) && (packetCallbacks.containsKey(id))) { + if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) { + ((OnMessagePacketReceived) packetCallbacks.get(id)) + .onMessagePacketReceived(account, packet); + } + packetCallbacks.remove(id); + } else if (this.messageListener != null) { + this.messageListener.onMessagePacketReceived(account, packet); + } } private void processPresence(Tag currentTag) throws XmlPullParserException, - IOException { - PresencePacket packet = (PresencePacket) processPacket(currentTag, - PACKET_PRESENCE); - String id = packet.getAttribute("id"); - if ((id != null) && (packetCallbacks.containsKey(id))) { - if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) { - ((OnPresencePacketReceived) packetCallbacks.get(id)) - .onPresencePacketReceived(account, packet); - } - packetCallbacks.remove(id); - } else if (this.presenceListener != null) { - this.presenceListener.onPresencePacketReceived(account, packet); - } + IOException { + PresencePacket packet = (PresencePacket) processPacket(currentTag, + PACKET_PRESENCE); + String id = packet.getAttribute("id"); + if ((id != null) && (packetCallbacks.containsKey(id))) { + if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) { + ((OnPresencePacketReceived) packetCallbacks.get(id)) + .onPresencePacketReceived(account, packet); + } + packetCallbacks.remove(id); + } else if (this.presenceListener != null) { + this.presenceListener.onPresencePacketReceived(account, packet); + } } private void sendCompressionZlib() throws IOException { @@ -507,19 +528,19 @@ public class XmppConnection implements Runnable { tagWriter.writeElement(compress); } - private void switchOverToZLib(Tag currentTag) - throws XmlPullParserException, IOException, - NoSuchAlgorithmException { - tagReader.readTag(); // read tag close - tagWriter.setOutputStream(new ZLibOutputStream(tagWriter - .getOutputStream())); - tagReader - .setInputStream(new ZLibInputStream(tagReader.getInputStream())); - - sendStartStream(); - Log.d(Config.LOGTAG, account.getJid() + ": compression enabled"); - usingCompression = true; - processStream(tagReader.readTag()); + private void switchOverToZLib(final Tag currentTag) + throws XmlPullParserException, IOException, + NoSuchAlgorithmException { + tagReader.readTag(); // read tag close + tagWriter.setOutputStream(new ZLibOutputStream(tagWriter + .getOutputStream())); + tagReader + .setInputStream(new ZLibInputStream(tagReader.getInputStream())); + + sendStartStream(); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": compression enabled"); + enabledCompression = true; + processStream(tagReader.readTag()); } private void sendStartTLS() throws IOException { @@ -530,116 +551,121 @@ public class XmppConnection implements Runnable { private SharedPreferences getPreferences() { return PreferenceManager - .getDefaultSharedPreferences(applicationContext); + .getDefaultSharedPreferences(applicationContext); } private boolean enableLegacySSL() { return getPreferences().getBoolean("enable_legacy_ssl", false); } - private void switchOverToTls(Tag currentTag) throws XmlPullParserException, - IOException { - tagReader.readTag(); - try { - SSLContext sc = SSLContext.getInstance("TLS"); - sc.init(null, - new X509TrustManager[]{this.mXmppConnectionService.getMemorizingTrustManager()}, - mXmppConnectionService.getRNG()); - SSLSocketFactory factory = sc.getSocketFactory(); - - if (factory == null) { - throw new IOException("SSLSocketFactory was null"); - } - - HostnameVerifier verifier = this.mXmppConnectionService.getMemorizingTrustManager().wrapHostnameVerifier(new StrictHostnameVerifier()); - - if (socket == null) { - throw new IOException("socket was null"); - } - SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket, - socket.getInetAddress().getHostAddress(), socket.getPort(), - true); - - // Support all protocols except legacy SSL. - // The min SDK version prevents us having to worry about SSLv2. In - // future, this may be - // true of SSLv3 as well. - final String[] supportProtocols; - if (enableLegacySSL()) { - supportProtocols = sslSocket.getSupportedProtocols(); - } else { - final List<String> supportedProtocols = new LinkedList<String>( - Arrays.asList(sslSocket.getSupportedProtocols())); - supportedProtocols.remove("SSLv3"); - supportProtocols = new String[supportedProtocols.size()]; - supportedProtocols.toArray(supportProtocols); - } - sslSocket.setEnabledProtocols(supportProtocols); - - if (verifier != null - && !verifier.verify(account.getServer(), - sslSocket.getSession())) { - sslSocket.close(); - throw new IOException("host mismatch in TLS connection"); - } - tagReader.setInputStream(sslSocket.getInputStream()); - tagWriter.setOutputStream(sslSocket.getOutputStream()); - sendStartStream(); - Log.d(Config.LOGTAG, account.getJid() - + ": TLS connection established"); - usingEncryption = true; - processStream(tagReader.readTag()); - sslSocket.close(); - } catch (NoSuchAlgorithmException e1) { - e1.printStackTrace(); - } catch (KeyManagementException e) { - e.printStackTrace(); - } - } + private void switchOverToTls(final Tag currentTag) throws XmlPullParserException, + IOException { + tagReader.readTag(); + try { + SSLContext sc = SSLContext.getInstance("TLS"); + sc.init(null, + new X509TrustManager[]{this.mXmppConnectionService.getMemorizingTrustManager()}, + mXmppConnectionService.getRNG()); + SSLSocketFactory factory = sc.getSocketFactory(); + + if (factory == null) { + throw new IOException("SSLSocketFactory was null"); + } - private void sendSaslAuthPlain() throws IOException { - String saslString = CryptoHelper.saslPlain(account.getUsername(), - account.getPassword()); - Element auth = new Element("auth"); - auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); - auth.setAttribute("mechanism", "PLAIN"); - auth.setContent(saslString); - tagWriter.writeElement(auth); - } + final HostnameVerifier verifier = this.mXmppConnectionService.getMemorizingTrustManager().wrapHostnameVerifier(new StrictHostnameVerifier()); - private void sendSaslAuthDigestMd5() throws IOException { - Element auth = new Element("auth"); - auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); - auth.setAttribute("mechanism", "DIGEST-MD5"); - tagWriter.writeElement(auth); + if (socket == null) { + throw new IOException("socket was null"); + } + final SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket, + socket.getInetAddress().getHostAddress(), socket.getPort(), + true); + + // Support all protocols except legacy SSL. + // The min SDK version prevents us having to worry about SSLv2. In + // future, this may be true of SSLv3 as well. + final String[] supportProtocols; + if (enableLegacySSL()) { + supportProtocols = sslSocket.getSupportedProtocols(); + } else { + final List<String> supportedProtocols = new LinkedList<>( + Arrays.asList(sslSocket.getSupportedProtocols())); + supportedProtocols.remove("SSLv3"); + supportProtocols = new String[supportedProtocols.size()]; + supportedProtocols.toArray(supportProtocols); + } + sslSocket.setEnabledProtocols(supportProtocols); + + if (verifier != null + && !verifier.verify(account.getServer().getDomainpart(), + sslSocket.getSession())) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": TLS certificate verification failed"); + disconnect(true); + changeStatus(Account.State.SECURITY_ERROR); + } + tagReader.setInputStream(sslSocket.getInputStream()); + tagWriter.setOutputStream(sslSocket.getOutputStream()); + sendStartStream(); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + + ": TLS connection established"); + enabledEncryption = true; + processStream(tagReader.readTag()); + sslSocket.close(); + } catch (final NoSuchAlgorithmException | KeyManagementException e1) { + e1.printStackTrace(); + } } private void processStreamFeatures(Tag currentTag) - throws XmlPullParserException, IOException { + throws XmlPullParserException, IOException { this.streamFeatures = tagReader.readElement(currentTag); - if (this.streamFeatures.hasChild("starttls") && !usingEncryption) { + if (this.streamFeatures.hasChild("starttls") && !enabledEncryption) { sendStartTLS(); } else if (compressionAvailable()) { sendCompressionZlib(); } else if (this.streamFeatures.hasChild("register") && account.isOptionSet(Account.OPTION_REGISTER) - && usingEncryption) { + && enabledEncryption) { sendRegistryRequest(); } else if (!this.streamFeatures.hasChild("register") && account.isOptionSet(Account.OPTION_REGISTER)) { - changeStatus(Account.STATUS_REGISTRATION_NOT_SUPPORTED); + changeStatus(Account.State.REGISTRATION_NOT_SUPPORTED); disconnect(true); } else if (this.streamFeatures.hasChild("mechanisms") - && shouldAuthenticate && usingEncryption) { - List<String> mechanisms = extractMechanisms(streamFeatures + && shouldAuthenticate && enabledEncryption) { + final List<String> mechanisms = extractMechanisms(streamFeatures .findChild("mechanisms")); - if (mechanisms.contains("PLAIN")) { - sendSaslAuthPlain(); + final Element auth = new Element("auth"); + auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); + if (mechanisms.contains("SCRAM-SHA-1")) { + saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG()); } else if (mechanisms.contains("DIGEST-MD5")) { - sendSaslAuthDigestMd5(); + saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG()); + } else if (mechanisms.contains("PLAIN")) { + saslMechanism = new Plain(tagWriter, account); } + final JSONObject keys = account.getKeys(); + try { + if (keys.has(Account.PINNED_MECHANISM_KEY) && + keys.getInt(Account.PINNED_MECHANISM_KEY) > saslMechanism.getPriority() ) { + Log.e(Config.LOGTAG, "Auth failed. Authentication mechanism " + saslMechanism.getMechanism() + + " has lower priority (" + String.valueOf(saslMechanism.getPriority()) + + ") than pinned priority (" + keys.getInt(Account.PINNED_MECHANISM_KEY) + + "). Possible downgrade attack?"); + disconnect(true); + changeStatus(Account.State.SECURITY_ERROR); + } + } catch (final JSONException e) { + Log.d(Config.LOGTAG, "Parse error while checking pinned auth mechanism"); + } + Log.d(Config.LOGTAG,account.getJid().toString()+": Authenticating with " + saslMechanism.getMechanism()); + auth.setAttribute("mechanism", saslMechanism.getMechanism()); + if (!saslMechanism.getClientFirstMessage().isEmpty()) { + auth.setContent(saslMechanism.getClientFirstMessage()); + } + tagWriter.writeElement(auth); } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" - + smVersion) + + smVersion) && streamId != null) { ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived, smVersion); @@ -647,15 +673,14 @@ public class XmppConnection implements Runnable { } else if (this.streamFeatures.hasChild("bind") && shouldBind) { sendBindRequest(); } else { - Log.d(Config.LOGTAG, account.getJid() - + ": incompatible server. disconnecting"); disconnect(true); + changeStatus(Account.State.INCOMPATIBLE_SERVER); } } private boolean compressionAvailable() { if (!this.streamFeatures.hasChild("compression", - "http://jabber.org/features/compress")) + "http://jabber.org/features/compress")) return false; if (!ZLibOutputStream.SUPPORTED) return false; @@ -676,7 +701,7 @@ public class XmppConnection implements Runnable { } private List<String> extractMechanisms(Element stream) { - ArrayList<String> mechanisms = new ArrayList<String>(stream + ArrayList<String> mechanisms = new ArrayList<>(stream .getChildren().size()); for (Element child : stream.getChildren()) { mechanisms.add(child.getContent()); @@ -697,35 +722,35 @@ public class XmppConnection implements Runnable { && (packet.query().hasChild("password"))) { IqPacket register = new IqPacket(IqPacket.TYPE_SET); Element username = new Element("username") - .setContent(account.getUsername()); + .setContent(account.getUsername()); Element password = new Element("password") - .setContent(account.getPassword()); + .setContent(account.getPassword()); register.query("jabber:iq:register").addChild(username); register.query().addChild(password); sendIqPacket(register, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, - IqPacket packet) { + IqPacket packet) { if (packet.getType() == IqPacket.TYPE_RESULT) { account.setOption(Account.OPTION_REGISTER, false); - changeStatus(Account.STATUS_REGISTRATION_SUCCESSFULL); + changeStatus(Account.State.REGISTRATION_SUCCESSFUL); } else if (packet.hasChild("error") && (packet.findChild("error") - .hasChild("conflict"))) { - changeStatus(Account.STATUS_REGISTRATION_CONFLICT); + .hasChild("conflict"))) { + changeStatus(Account.State.REGISTRATION_CONFLICT); } else { - changeStatus(Account.STATUS_REGISTRATION_FAILED); + changeStatus(Account.State.REGISTRATION_FAILED); Log.d(Config.LOGTAG, packet.toString()); } disconnect(true); } }); } else { - changeStatus(Account.STATUS_REGISTRATION_FAILED); + changeStatus(Account.State.REGISTRATION_FAILED); disconnect(true); - Log.d(Config.LOGTAG, account.getJid() + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not register. instructions are" + instructions.getContent()); } @@ -736,15 +761,19 @@ public class XmppConnection implements Runnable { private void sendBindRequest() throws IOException { IqPacket iq = new IqPacket(IqPacket.TYPE_SET); iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind") - .addChild("resource").setContent(account.getResource()); + .addChild("resource").setContent(account.getResource()); this.sendUnboundIqPacket(iq, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { Element bind = packet.findChild("bind"); if (bind != null) { - Element jid = bind.findChild("jid"); + final Element jid = bind.findChild("jid"); if (jid != null && jid.getContent() != null) { - account.setResource(jid.getContent().split("/", 2)[1]); + try { + account.setResource(Jid.fromString(jid.getContent()).getResourcepart()); + } catch (final InvalidJidException e) { + // TODO: Handle the case where an external JID is technically invalid? + } if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) { smVersion = 3; EnablePacket enable = new EnablePacket(smVersion); @@ -752,13 +781,15 @@ public class XmppConnection implements Runnable { stanzasSent = 0; messageReceipts.clear(); } else if (streamFeatures.hasChild("sm", - "urn:xmpp:sm:2")) { + "urn:xmpp:sm:2")) { smVersion = 2; EnablePacket enable = new EnablePacket(smVersion); tagWriter.writeStanzaAsync(enable); stanzasSent = 0; messageReceipts.clear(); } + enabledCarbons = false; + disco.clear(); sendServiceDiscoveryInfo(account.getServer()); sendServiceDiscoveryItems(account.getServer()); if (bindListener != null) { @@ -774,7 +805,7 @@ public class XmppConnection implements Runnable { } }); if (this.streamFeatures.hasChild("session")) { - Log.d(Config.LOGTAG, account.getJid() + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": sending deprecated session"); IqPacket startSession = new IqPacket(IqPacket.TYPE_SET); startSession.addChild("session", @@ -783,49 +814,61 @@ public class XmppConnection implements Runnable { } } - private void sendServiceDiscoveryInfo(final String server) { - IqPacket iq = new IqPacket(IqPacket.TYPE_GET); - iq.setTo(server); - iq.query("http://jabber.org/protocol/disco#info"); - this.sendIqPacket(iq, new OnIqPacketReceived() { + private void sendServiceDiscoveryInfo(final Jid server) { + if (disco.containsKey(server.toDomainJid().toString())) { + if (account.getServer().equals(server.toDomainJid())) { + enableAdvancedStreamFeatures(); + } + } else { + final IqPacket iq = new IqPacket(IqPacket.TYPE_GET); + iq.setTo(server.toDomainJid()); + iq.query("http://jabber.org/protocol/disco#info"); + this.sendIqPacket(iq, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - List<Element> elements = packet.query().getChildren(); - List<String> features = new ArrayList<String>(); - for (int i = 0; i < elements.size(); ++i) { - if (elements.get(i).getName().equals("feature")) { - features.add(elements.get(i).getAttribute("var")); + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + final List<Element> elements = packet.query().getChildren(); + final List<String> features = new ArrayList<>(); + for (Element element : elements) { + if (element.getName().equals("feature")) { + features.add(element.getAttribute("var")); + } } - } - disco.put(server, features); + disco.put(server.toDomainJid().toString(), features); - if (account.getServer().equals(server)) { - enableAdvancedStreamFeatures(); + if (account.getServer().equals(server.toDomainJid())) { + enableAdvancedStreamFeatures(); + } } - } - }); + }); + } } private void enableAdvancedStreamFeatures() { if (getFeatures().carbons()) { - sendEnableCarbons(); + if (!enabledCarbons) { + sendEnableCarbons(); + } } } - private void sendServiceDiscoveryItems(final String server) { - IqPacket iq = new IqPacket(IqPacket.TYPE_GET); - iq.setTo(server); + private void sendServiceDiscoveryItems(final Jid server) { + final IqPacket iq = new IqPacket(IqPacket.TYPE_GET); + iq.setTo(server.toDomainJid()); iq.query("http://jabber.org/protocol/disco#items"); this.sendIqPacket(iq, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { List<Element> elements = packet.query().getChildren(); - for (int i = 0; i < elements.size(); ++i) { - if (elements.get(i).getName().equals("item")) { - String jid = elements.get(i).getAttribute("jid"); - sendServiceDiscoveryInfo(jid); + for (Element element : elements) { + if (element.getName().equals("item")) { + final String jid = element.getAttribute("jid"); + try { + sendServiceDiscoveryInfo(Jid.fromString(jid).toDomainJid()); + } catch (final InvalidJidException ignored) { + // TODO: Handle the case where an external JID is technically invalid? + } } } } @@ -840,10 +883,11 @@ public class XmppConnection implements Runnable { @Override public void onIqPacketReceived(Account account, IqPacket packet) { if (!packet.hasChild("error")) { - Log.d(Config.LOGTAG, account.getJid() + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": successfully enabled carbons"); + enabledCarbons = true; } else { - Log.d(Config.LOGTAG, account.getJid() + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": error enableing carbons " + packet.toString()); } } @@ -851,21 +895,21 @@ public class XmppConnection implements Runnable { } private void processStreamError(Tag currentTag) - throws XmlPullParserException, IOException { + throws XmlPullParserException, IOException { Element streamError = tagReader.readElement(currentTag); if (streamError != null && streamError.hasChild("conflict")) { - String resource = account.getResource().split("\\.")[0]; + final String resource = account.getResource().split("\\.")[0]; account.setResource(resource + "." + nextRandomId()); Log.d(Config.LOGTAG, - account.getJid() + ": switching resource due to conflict (" - + account.getResource() + ")"); + account.getJid().toBareJid() + ": switching resource due to conflict (" + + account.getResource() + ")"); } } private void sendStartStream() throws IOException { Tag stream = Tag.start("stream:stream"); - stream.setAttribute("from", account.getJid()); - stream.setAttribute("to", account.getServer()); + stream.setAttribute("from", account.getJid().toBareJid().toString()); + stream.setAttribute("to", account.getServer().toString()); stream.setAttribute("version", "1.0"); stream.setAttribute("xml:lang", "en"); stream.setAttribute("xmlns", "jabber:client"); @@ -882,7 +926,7 @@ public class XmppConnection implements Runnable { String id = nextRandomId(); packet.setAttribute("id", id); } - packet.setFrom(account.getFullJid()); + packet.setFrom(account.getJid()); this.sendPacket(packet, callback); } @@ -903,11 +947,11 @@ public class XmppConnection implements Runnable { } private synchronized void sendPacket(final AbstractStanza packet, - PacketReceived callback) { + PacketReceived callback) { if (packet.getName().equals("iq") || packet.getName().equals("message") || packet.getName().equals("presence")) { ++stanzasSent; - } + } tagWriter.writeStanzaAsync(packet); if (packet instanceof MessagePacket && packet.getId() != null && this.streamId != null) { @@ -915,7 +959,7 @@ public class XmppConnection implements Runnable { + stanzasSent); this.messageReceipts.put(stanzasSent, packet.getId()); tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion)); - } + } if (callback != null) { if (packet.getId() == null) { packet.setId(nextRandomId()); @@ -929,7 +973,7 @@ public class XmppConnection implements Runnable { tagWriter.writeStanzaAsync(new RequestPacket(smVersion)); } else { IqPacket iq = new IqPacket(IqPacket.TYPE_GET); - iq.setFrom(account.getFullJid()); + iq.setFrom(account.getJid()); iq.addChild("ping", "urn:xmpp:ping"); this.sendIqPacket(iq, null); } @@ -939,22 +983,22 @@ public class XmppConnection implements Runnable { public void setOnMessagePacketReceivedListener( OnMessagePacketReceived listener) { this.messageListener = listener; - } + } public void setOnUnregisteredIqPacketReceivedListener( OnIqPacketReceived listener) { this.unregisteredIqListener = listener; - } + } public void setOnPresencePacketReceivedListener( OnPresencePacketReceived listener) { this.presenceListener = listener; - } + } public void setOnJinglePacketReceivedListener( OnJinglePacketReceived listener) { this.jingleListener = listener; - } + } public void setOnStatusChangedListener(OnStatusChanged listener) { this.statusListener = listener; @@ -969,7 +1013,7 @@ public class XmppConnection implements Runnable { } public void disconnect(boolean force) { - Log.d(Config.LOGTAG, account.getJid() + ": disconnecting"); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": disconnecting"); try { if (force) { socket.close(); @@ -1003,7 +1047,7 @@ public class XmppConnection implements Runnable { } public List<String> findDiscoItemsByFeature(String feature) { - List<String> items = new ArrayList<String>(); + final List<String> items = new ArrayList<>(); for (Entry<String, List<String>> cursor : disco.entrySet()) { if (cursor.getValue().contains(feature)) { items.add(cursor.getKey()); @@ -1079,11 +1123,9 @@ public class XmppConnection implements Runnable { this.connection = connection; } - private boolean hasDiscoFeature(String server, String feature) { - if (!connection.disco.containsKey(server)) { - return false; - } - return connection.disco.get(server).contains(feature); + private boolean hasDiscoFeature(final Jid server, final String feature) { + return connection.disco.containsKey(server.toDomainJid().toString()) && + connection.disco.get(server.toDomainJid().toString()).contains(feature); } public boolean carbons() { @@ -1095,12 +1137,7 @@ public class XmppConnection implements Runnable { } public boolean csi() { - if (connection.streamFeatures == null) { - return false; - } else { - return connection.streamFeatures.hasChild("csi", - "urn:xmpp:csi:0"); - } + return connection.streamFeatures != null && connection.streamFeatures.hasChild("csi", "urn:xmpp:csi:0"); } public boolean pubsub() { @@ -1113,20 +1150,16 @@ public class XmppConnection implements Runnable { } public boolean rosterVersioning() { - if (connection.streamFeatures == null) { - return false; - } else { - return connection.streamFeatures.hasChild("ver"); - } + return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver"); } public boolean streamhost() { return connection - .findDiscoItemByFeature("http://jabber.org/protocol/bytestreams") != null; + .findDiscoItemByFeature("http://jabber.org/protocol/bytestreams") != null; } public boolean compression() { - return connection.usingCompression; + return connection.enabledCompression; } } } |