aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/eu/siacs/conversations/xmpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/eu/siacs/conversations/xmpp')
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnAdvancedStreamFeaturesLoaded.java7
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java7
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java7
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java8
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnKeyStatusUpdated.java7
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java7
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java8
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java8
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java7
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnUpdateBlocklist.java13
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java5
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java1574
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/chatstate/ChatState.java32
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/forms/Data.java99
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/forms/Field.java81
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jid/InvalidJidException.java49
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java226
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java147
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java1025
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java170
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java239
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java217
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java15
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java9
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java9
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java5
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java7
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java103
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java96
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java13
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java102
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java31
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java50
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java69
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java99
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java8
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java10
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java10
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java13
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java13
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java12
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java14
42 files changed, 4631 insertions, 0 deletions
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnAdvancedStreamFeaturesLoaded.java b/src/main/java/eu/siacs/conversations/xmpp/OnAdvancedStreamFeaturesLoaded.java
new file mode 100644
index 00000000..e45eba73
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnAdvancedStreamFeaturesLoaded.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Account;
+
+public interface OnAdvancedStreamFeaturesLoaded {
+ public void onAdvancedStreamFeaturesAvailable(final Account account);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java b/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java
new file mode 100644
index 00000000..f09cf33d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Account;
+
+public interface OnBindListener {
+ public void onBind(Account account);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java b/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java
new file mode 100644
index 00000000..20b17f02
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Contact;
+
+public interface OnContactStatusChanged {
+ public void onContactStatusChanged(final Contact contact, final boolean online);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java
new file mode 100644
index 00000000..a4cff986
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java
@@ -0,0 +1,8 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public interface OnIqPacketReceived extends PacketReceived {
+ public void onIqPacketReceived(Account account, IqPacket packet);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnKeyStatusUpdated.java b/src/main/java/eu/siacs/conversations/xmpp/OnKeyStatusUpdated.java
new file mode 100644
index 00000000..e7fc582e
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnKeyStatusUpdated.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+
+public interface OnKeyStatusUpdated {
+ public void onKeyStatusUpdated(AxolotlService.FetchStatus report);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java b/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java
new file mode 100644
index 00000000..5f670d93
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Account;
+
+public interface OnMessageAcknowledged {
+ public void onMessageAcknowledged(Account account, String id);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java
new file mode 100644
index 00000000..325e945f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java
@@ -0,0 +1,8 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+
+public interface OnMessagePacketReceived extends PacketReceived {
+ public void onMessagePacketReceived(Account account, MessagePacket packet);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java
new file mode 100644
index 00000000..95c1acfc
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java
@@ -0,0 +1,8 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
+
+public interface OnPresencePacketReceived extends PacketReceived {
+ public void onPresencePacketReceived(Account account, PresencePacket packet);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java b/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java
new file mode 100644
index 00000000..ad1d98cb
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Account;
+
+public interface OnStatusChanged {
+ public void onStatusChanged(Account account);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnUpdateBlocklist.java b/src/main/java/eu/siacs/conversations/xmpp/OnUpdateBlocklist.java
new file mode 100644
index 00000000..92e72cfa
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnUpdateBlocklist.java
@@ -0,0 +1,13 @@
+package eu.siacs.conversations.xmpp;
+
+public interface OnUpdateBlocklist {
+ // Use an enum instead of a boolean to make sure we don't run into the boolean trap
+ // (`onUpdateBlocklist(true)' doesn't read well, and could be confusing).
+ public static enum Status {
+ BLOCKED,
+ UNBLOCKED
+ }
+
+ @SuppressWarnings("MethodNameSameAsClassName")
+ public void OnUpdateBlocklist(final Status status);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java
new file mode 100644
index 00000000..d4502d73
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java
@@ -0,0 +1,5 @@
+package eu.siacs.conversations.xmpp;
+
+public abstract interface PacketReceived {
+
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
new file mode 100644
index 00000000..a5d0321d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
@@ -0,0 +1,1574 @@
+package eu.siacs.conversations.xmpp;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.PowerManager.WakeLock;
+import android.os.SystemClock;
+import android.security.KeyChain;
+import android.util.Base64;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.net.ConnectException;
+import java.net.IDN;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.net.URL;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.TreeSet;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509KeyManager;
+import javax.net.ssl.X509TrustManager;
+
+import de.duenndns.ssl.MemorizingTrustManager;
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.thedevstack.conversationsplus.dto.SrvRecord;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.XmppDomainVerifier;
+import eu.siacs.conversations.crypto.sasl.DigestMd5;
+import eu.siacs.conversations.crypto.sasl.External;
+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.entities.Message;
+import eu.siacs.conversations.entities.ServiceDiscoveryResult;
+import eu.siacs.conversations.generator.IqGenerator;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.DNSHelper;
+import eu.siacs.conversations.utils.SSLSocketHelper;
+import eu.siacs.conversations.utils.SocksSocketFactory;
+import eu.siacs.conversations.utils.Xmlns;
+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.forms.Data;
+import eu.siacs.conversations.xmpp.forms.Field;
+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.AbstractAcknowledgeableStanza;
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
+import eu.siacs.conversations.xmpp.stanzas.csi.ActivePacket;
+import eu.siacs.conversations.xmpp.stanzas.csi.InactivePacket;
+import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
+import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
+import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
+import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
+
+public class XmppConnection implements Runnable {
+ private static final int DEFAULT_PORT = 5222;
+ private static final int PACKET_IQ = 0;
+ private static final int PACKET_MESSAGE = 1;
+ private static final int PACKET_PRESENCE = 2;
+ protected Account account;
+ private final WakeLock wakeLock;
+ private Socket socket;
+ private XmlReader tagReader;
+ private TagWriter tagWriter;
+ private final Features features = new Features(this);
+ private boolean needsBinding = true;
+ private boolean shouldAuthenticate = true;
+ private Element streamFeatures;
+ private final HashMap<Jid, ServiceDiscoveryResult> disco = new HashMap<>();
+
+ private String streamId = null;
+ private int smVersion = 3;
+ private final SparseArray<AbstractAcknowledgeableStanza> mStanzaQueue = new SparseArray<>();
+
+ private int stanzasReceived = 0;
+ private int stanzasSent = 0;
+ private long lastPacketReceived = 0;
+ private long lastPingSent = 0;
+ private long lastConnect = 0;
+ private long lastSessionStarted = 0;
+ private long lastDiscoStarted = 0;
+ private AtomicInteger mPendingServiceDiscoveries = new AtomicInteger(0);
+ private AtomicBoolean mIsServiceItemsDiscoveryPending = new AtomicBoolean(true);
+ private boolean mWaitForDisco = true;
+ private final ArrayList<String> mPendingServiceDiscoveriesIds = new ArrayList<>();
+ private boolean mInteractive = false;
+ private int attempt = 0;
+ private final Hashtable<String, Pair<IqPacket, OnIqPacketReceived>> packetCallbacks = new Hashtable<>();
+ private OnPresencePacketReceived presenceListener = null;
+ private OnJinglePacketReceived jingleListener = null;
+ private OnIqPacketReceived unregisteredIqListener = null;
+ private OnMessagePacketReceived messageListener = null;
+ private OnStatusChanged statusListener = null;
+ private OnBindListener bindListener = null;
+ private final ArrayList<OnAdvancedStreamFeaturesLoaded> advancedStreamFeaturesLoadedListeners = new ArrayList<>();
+ private OnMessageAcknowledged acknowledgedListener = null;
+ private XmppConnectionService mXmppConnectionService = null;
+
+ private SaslMechanism saslMechanism;
+
+ private X509KeyManager mKeyManager = new X509KeyManager() {
+ @Override
+ public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
+ return account.getPrivateKeyAlias();
+ }
+
+ @Override
+ public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
+ return null;
+ }
+
+ @Override
+ public X509Certificate[] getCertificateChain(String alias) {
+ try {
+ return KeyChain.getCertificateChain(mXmppConnectionService, alias);
+ } catch (Exception e) {
+ return new X509Certificate[0];
+ }
+ }
+
+ @Override
+ public String[] getClientAliases(String s, Principal[] principals) {
+ return new String[0];
+ }
+
+ @Override
+ public String[] getServerAliases(String s, Principal[] principals) {
+ return new String[0];
+ }
+
+ @Override
+ public PrivateKey getPrivateKey(String alias) {
+ try {
+ return KeyChain.getPrivateKey(mXmppConnectionService, alias);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+ };
+ private Identity mServerIdentity = Identity.UNKNOWN;
+
+ private OnIqPacketReceived createPacketReceiveHandler() {
+ return new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ account.setOption(Account.OPTION_REGISTER,
+ false);
+ changeStatus(Account.State.REGISTRATION_SUCCESSFUL);
+ } else if (packet.hasChild("error")
+ && (packet.findChild("error")
+ .hasChild("conflict"))) {
+ changeStatus(Account.State.REGISTRATION_CONFLICT);
+ } else {
+ changeStatus(Account.State.REGISTRATION_FAILED);
+ Log.d(Config.LOGTAG, packet.toString());
+ }
+ disconnect(true);
+ }
+ };
+ }
+
+ public XmppConnection(final Account account, final XmppConnectionService service) {
+ this.account = account;
+ this.wakeLock = ConversationsPlusApplication.createPartialWakeLock(account.getJid().toBareJid().toString());
+ tagWriter = new TagWriter();
+ mXmppConnectionService = service;
+ }
+
+ protected void changeStatus(final Account.State nextStatus) {
+ if (account.getStatus() != nextStatus) {
+ if ((nextStatus == Account.State.OFFLINE)
+ && (account.getStatus() != Account.State.CONNECTING)
+ && (account.getStatus() != Account.State.ONLINE)
+ && (account.getStatus() != Account.State.DISABLED)) {
+ return;
+ }
+ if (nextStatus == Account.State.ONLINE) {
+ this.attempt = 0;
+ }
+ account.setStatus(nextStatus);
+ if (statusListener != null) {
+ statusListener.onStatusChanged(account);
+ }
+ }
+ }
+
+ public void prepareNewConnection() {
+ this.lastConnect = SystemClock.elapsedRealtime();
+ this.lastPingSent = SystemClock.elapsedRealtime();
+ this.lastDiscoStarted = Long.MAX_VALUE;
+ this.changeStatus(Account.State.CONNECTING);
+ }
+
+ protected void connect() {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": connecting");
+ features.encryptionEnabled = false;
+ this.attempt++;
+ switch (account.getJid().getDomainpart()) {
+ case "chat.facebook.com":
+ mServerIdentity = Identity.FACEBOOK;
+ break;
+ case "nimbuzz.com":
+ mServerIdentity = Identity.NIMBUZZ;
+ break;
+ default:
+ mServerIdentity = Identity.UNKNOWN;
+ break;
+ }
+ try {
+ shouldAuthenticate = needsBinding = !account.isOptionSet(Account.OPTION_REGISTER);
+ tagReader = new XmlReader(wakeLock);
+ tagWriter = new TagWriter();
+ this.changeStatus(Account.State.CONNECTING);
+ final boolean extended = ConversationsPlusPreferences.showConnectionOptions();
+ if (extended && account.getHostname() != null && !account.getHostname().isEmpty()) {
+ socket = new Socket();
+ try {
+ socket.connect(new InetSocketAddress(account.getHostname(), account.getPort()), Config.SOCKET_TIMEOUT * 1000);
+ } catch (IOException e) {
+ throw new UnknownHostException();
+ }
+ startXmpp();
+ } else if (DNSHelper.isIp(account.getServer().toString())) {
+ socket = new Socket();
+ try {
+ socket.connect(new InetSocketAddress(account.getServer().toString(), DEFAULT_PORT), Config.SOCKET_TIMEOUT * 1000);
+ } catch (IOException e) {
+ throw new UnknownHostException();
+ }
+ startXmpp();
+ } else {
+ final TreeSet<SrvRecord> srvRecords = DNSHelper.querySrvRecord(account.getServer());
+ if (srvRecords.isEmpty()) {
+ socket = new Socket();
+ try {
+ socket.connect(new InetSocketAddress(account.getServer().getDomainpart(), DEFAULT_PORT), Config.SOCKET_TIMEOUT * 1000);
+ } catch (IOException e) {
+ throw new UnknownHostException();
+ }
+ startXmpp();
+ } else {
+ for (SrvRecord srvRecord : srvRecords) {
+ // if tls is true, encryption is implied and must not be started
+ features.encryptionEnabled = srvRecord.isUseTls();
+ TlsFactoryVerifier tlsFactoryVerifier = null;
+ if (features.encryptionEnabled) {
+ try {
+ tlsFactoryVerifier = getTlsFactoryVerifier();
+ socket = tlsFactoryVerifier.factory.createSocket();
+
+ if (socket == null) {
+ throw new IOException("could not initialize ssl socket");
+ }
+
+ SSLSocketHelper.setSecurity((SSLSocket) socket);
+ SSLSocketHelper.setSNIHost(tlsFactoryVerifier.factory, (SSLSocket) socket, account.getServer().getDomainpart());
+ SSLSocketHelper.setAlpnProtocol(tlsFactoryVerifier.factory, (SSLSocket) socket, "xmpp-client");
+ } catch (SecurityException e) {
+ throw e;
+ } catch (KeyManagementException e) {
+ Logging.e("connection-init", "Error while creating TLS verifier factory: " + e.getMessage(), e);
+ throw new SecurityException();
+ }
+ } else {
+ socket = new Socket();
+ }
+
+ socket.connect(new InetSocketAddress(srvRecord.getName(), srvRecord.getPort()), Config.SOCKET_TIMEOUT * 1000);
+
+ if (null != tlsFactoryVerifier && !tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), ((SSLSocket) socket).getSession())) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed");
+ throw new SecurityException();
+ }
+
+ if (startXmpp()) {
+ break; // successfully connected to server that speaks xmpp
+ }
+ }
+ }
+ }
+ processStream();
+ } catch (final IncompatibleServerException e) {
+ this.changeStatus(Account.State.INCOMPATIBLE_SERVER);
+ } catch (final SecurityException e) {
+ this.changeStatus(Account.State.SECURITY_ERROR);
+ } catch (final UnauthorizedException e) {
+ this.changeStatus(Account.State.UNAUTHORIZED);
+ } catch (final UnknownHostException | ConnectException e) {
+ this.changeStatus(Account.State.SERVER_NOT_FOUND);
+ } catch (final IOException | XmlPullParserException | NoSuchAlgorithmException e) {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage());
+ this.changeStatus(Account.State.OFFLINE);
+ this.attempt--; //don't count attempt when reconnecting instantly anyway
+ } finally {
+ forceCloseSocket();
+ if (wakeLock.isHeld()) {
+ try {
+ wakeLock.release();
+ } catch (final RuntimeException ignored) {
+ }
+ }
+ }
+ }
+
+ /**
+ * Starts xmpp protocol, call after connecting to socket
+ * @return true if server returns with valid xmpp, false otherwise
+ * @throws IOException Unknown tag on connect
+ * @throws XmlPullParserException Bad Xml
+ * @throws NoSuchAlgorithmException Other error
+ */
+ private boolean startXmpp() throws IOException, XmlPullParserException, NoSuchAlgorithmException {
+ tagWriter.setOutputStream(socket.getOutputStream());
+ tagReader.setInputStream(socket.getInputStream());
+ tagWriter.beginDocument();
+ sendStartStream();
+ Tag nextTag;
+ while ((nextTag = tagReader.readTag()) != null) {
+ if (nextTag.isStart("stream")) {
+ return true;
+ } else {
+ throw new IOException("unknown tag on connect");
+ }
+ }
+ if (socket.isConnected()) {
+ socket.close();
+ }
+ return false;
+ }
+
+ private static class TlsFactoryVerifier {
+ private final SSLSocketFactory factory;
+ private final HostnameVerifier verifier;
+
+ public TlsFactoryVerifier(final SSLSocketFactory factory, final HostnameVerifier verifier) throws IOException {
+ this.factory = factory;
+ this.verifier = verifier;
+ if (factory == null || verifier == null) {
+ throw new IOException("could not setup ssl");
+ }
+ }
+ }
+
+ private TlsFactoryVerifier getTlsFactoryVerifier() throws NoSuchAlgorithmException, KeyManagementException, IOException {
+ final SSLContext sc = SSLSocketHelper.getSSLContext();
+ MemorizingTrustManager trustManager = ConversationsPlusApplication.getMemorizingTrustManager();
+ KeyManager[] keyManager;
+ if (account.getPrivateKeyAlias() != null && account.getPassword().isEmpty()) {
+ keyManager = new KeyManager[]{mKeyManager};
+ } else {
+ keyManager = null;
+ }
+ sc.init(keyManager, new X509TrustManager[]{mInteractive ? trustManager : trustManager.getNonInteractive()}, ConversationsPlusApplication.getSecureRandom());
+ final SSLSocketFactory factory = sc.getSocketFactory();
+ final HostnameVerifier verifier;
+ if (mInteractive) {
+ verifier = trustManager.wrapHostnameVerifier(new XmppDomainVerifier());
+ } else {
+ verifier = trustManager.wrapHostnameVerifierNonInteractive(new XmppDomainVerifier());
+ }
+
+ return new TlsFactoryVerifier(factory, verifier);
+ }
+
+ @Override
+ public void run() {
+ forceCloseSocket();
+ connect();
+ }
+
+ private void processStream() 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("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();
+ final Tag tag = tagReader.readTag();
+ if (tag != null && tag.isStart("stream")) {
+ processStream();
+ } else {
+ throw new IOException("server didn't restart stream after successful auth");
+ }
+ break;
+ } else if (nextTag.isStart("failure")) {
+ throw new UnauthorizedException();
+ } 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")) {
+ final 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 management(" + smVersion + ") enabled");
+ }
+ this.stanzasReceived = 0;
+ final RequestPacket r = new RequestPacket(smVersion);
+ tagWriter.writeStanzaAsync(r);
+ } else if (nextTag.isStart("resumed")) {
+ lastPacketReceived = SystemClock.elapsedRealtime();
+ final Element resumed = tagReader.readElement(nextTag);
+ final String h = resumed.getAttribute("h");
+ try {
+ final 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");
+ }
+ acknowledgeStanzaUpTo(serverCount);
+ ArrayList<AbstractAcknowledgeableStanza> failedStanzas = new ArrayList<>();
+ for(int i = 0; i < this.mStanzaQueue.size(); ++i) {
+ failedStanzas.add(mStanzaQueue.valueAt(i));
+ }
+ mStanzaQueue.clear();
+ Log.d(Config.LOGTAG,"resending "+failedStanzas.size()+" stanzas");
+ for(AbstractAcknowledgeableStanza packet : failedStanzas) {
+ if (packet instanceof MessagePacket) {
+ MessagePacket message = (MessagePacket) packet;
+ mXmppConnectionService.markMessage(account,
+ message.getTo().toBareJid(),
+ message.getId(),
+ Message.STATUS_UNSEND);
+ }
+ sendPacket(packet);
+ }
+ } catch (final NumberFormatException ignored) {
+ }
+ Log.d(Config.LOGTAG, account.getJid().toBareJid()+ ": online with resource " + account.getResource());
+ changeStatus(Account.State.ONLINE);
+ } else if (nextTag.isStart("r")) {
+ tagReader.readElement(nextTag);
+ if (Config.EXTENDED_SM_LOGGING) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": acknowledging stanza #" + this.stanzasReceived);
+ }
+ final AckPacket ack = new AckPacket(this.stanzasReceived, smVersion);
+ tagWriter.writeStanzaAsync(ack);
+ } else if (nextTag.isStart("a")) {
+ final Element ack = tagReader.readElement(nextTag);
+ lastPacketReceived = SystemClock.elapsedRealtime();
+ try {
+ final int serverSequence = Integer.parseInt(ack.getAttribute("h"));
+ acknowledgeStanzaUpTo(serverSequence);
+ } catch (NumberFormatException e) {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": server send ack without sequence number");
+ }
+ } else if (nextTag.isStart("failed")) {
+ tagReader.readElement(nextTag);
+ Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": resumption failed");
+ resetStreamId();
+ 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();
+ }
+ }
+
+ private void acknowledgeStanzaUpTo(int serverCount) {
+ for (int i = 0; i < mStanzaQueue.size(); ++i) {
+ if (serverCount >= mStanzaQueue.keyAt(i)) {
+ if (Config.EXTENDED_SM_LOGGING) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": server acknowledged stanza #" + mStanzaQueue.keyAt(i));
+ }
+ AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i);
+ if (stanza instanceof MessagePacket && acknowledgedListener != null) {
+ MessagePacket packet = (MessagePacket) stanza;
+ acknowledgedListener.onMessageAcknowledged(account, packet.getId());
+ }
+ mStanzaQueue.removeAt(i);
+ i--;
+ }
+ }
+ }
+
+ private Element processPacket(final Tag currentTag, final int packetType)
+ throws XmlPullParserException, IOException {
+ Element element;
+ switch (packetType) {
+ case PACKET_IQ:
+ element = new IqPacket();
+ break;
+ case PACKET_MESSAGE:
+ element = new MessagePacket();
+ break;
+ case PACKET_PRESENCE:
+ element = new PresencePacket();
+ break;
+ default:
+ return null;
+ }
+ element.setAttributes(currentTag.getAttributes());
+ Tag nextTag = tagReader.readTag();
+ if (nextTag == null) {
+ throw new IOException("interrupted mid tag");
+ }
+ while (!nextTag.isEnd(element.getName())) {
+ if (!nextTag.isNo()) {
+ final Element child = tagReader.readElement(nextTag);
+ final String type = currentTag.getAttribute("type");
+ if (packetType == PACKET_IQ
+ && "jingle".equals(child.getName())
+ && ("set".equalsIgnoreCase(type) || "get"
+ .equalsIgnoreCase(type))) {
+ element = new JinglePacket();
+ element.setAttributes(currentTag.getAttributes());
+ }
+ element.addChild(child);
+ }
+ nextTag = tagReader.readTag();
+ if (nextTag == null) {
+ throw new IOException("interrupted mid tag");
+ }
+ }
+ if (stanzasReceived == Integer.MAX_VALUE) {
+ resetStreamId();
+ throw new IOException("time to restart the session. cant handle >2 billion pcks");
+ }
+ ++stanzasReceived;
+ lastPacketReceived = SystemClock.elapsedRealtime();
+ return element;
+ }
+
+ private void processIq(final Tag currentTag) throws XmlPullParserException, IOException {
+ final 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 {
+ OnIqPacketReceived callback = null;
+ synchronized (this.packetCallbacks) {
+ if (packetCallbacks.containsKey(packet.getId())) {
+ final Pair<IqPacket, OnIqPacketReceived> packetCallbackDuple = packetCallbacks.get(packet.getId());
+ // Packets to the server should have responses from the server
+ if (packetCallbackDuple.first.toServer(account)) {
+ if (packet.fromServer(account) || mServerIdentity == Identity.FACEBOOK) {
+ callback = packetCallbackDuple.second;
+ packetCallbacks.remove(packet.getId());
+ } else {
+ Log.e(Config.LOGTAG, account.getJid().toBareJid().toString() + ": ignoring spoofed iq packet");
+ }
+ } else {
+ if (packet.getFrom().equals(packetCallbackDuple.first.getTo())) {
+ callback = packetCallbackDuple.second;
+ packetCallbacks.remove(packet.getId());
+ } else {
+ Logging.e(Config.LOGTAG, account.getJid().toBareJid().toString() + ": ignoring spoofed iq packet");
+ }
+ }
+ } else if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) {
+ callback = this.unregisteredIqListener;
+ }
+ }
+ if (callback != null) {
+ callback.onIqPacketReceived(account,packet);
+ }
+ }
+ }
+
+ private void processMessage(final Tag currentTag) throws XmlPullParserException, IOException {
+ final MessagePacket packet = (MessagePacket) processPacket(currentTag,PACKET_MESSAGE);
+ this.messageListener.onMessagePacketReceived(account, packet);
+ }
+
+ private void processPresence(final Tag currentTag) throws XmlPullParserException, IOException {
+ PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE);
+ this.presenceListener.onPresencePacketReceived(account, packet);
+ }
+
+ private void sendStartTLS() throws IOException {
+ final Tag startTLS = Tag.empty("starttls");
+ startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
+ tagWriter.writeTag(startTLS);
+ }
+
+
+
+ private void switchOverToTls(final Tag currentTag) throws XmlPullParserException, IOException {
+ tagReader.readTag();
+ try {
+ final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier();
+ final InetAddress address = socket == null ? null : socket.getInetAddress();
+
+ if (address == null) {
+ throw new IOException("could not setup ssl");
+ }
+
+ final SSLSocket sslSocket = (SSLSocket) tlsFactoryVerifier.factory.createSocket(socket, address.getHostAddress(), socket.getPort(), true);
+
+ if (sslSocket == null) {
+ throw new IOException("could not initialize ssl socket");
+ }
+
+ SSLSocketHelper.setSecurity(sslSocket);
+
+ if (!tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), sslSocket.getSession())) {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": TLS certificate verification failed");
+ throw new SecurityException();
+ }
+ tagReader.setInputStream(sslSocket.getInputStream());
+ tagWriter.setOutputStream(sslSocket.getOutputStream());
+ sendStartStream();
+ Log.d(Config.LOGTAG, account.getJid().toBareJid()+ ": TLS connection established");
+ features.encryptionEnabled = true;
+ final Tag tag = tagReader.readTag();
+ if (tag != null && tag.isStart("stream")) {
+ processStream();
+ } else {
+ throw new IOException("server didn't restart stream after STARTTLS");
+ }
+ sslSocket.close();
+ } catch (final NoSuchAlgorithmException | KeyManagementException e1) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed");
+ throw new SecurityException();
+ }
+ }
+
+ private void processStreamFeatures(final Tag currentTag)
+ throws XmlPullParserException, IOException {
+ this.streamFeatures = tagReader.readElement(currentTag);
+ if (this.streamFeatures.hasChild("starttls") && !features.encryptionEnabled) {
+ sendStartTLS();
+ } else if (this.streamFeatures.hasChild("register") && account.isOptionSet(Account.OPTION_REGISTER)) {
+ if (features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS) {
+ sendRegistryRequest();
+ } else {
+ throw new IncompatibleServerException();
+ }
+ } else if (!this.streamFeatures.hasChild("register")
+ && account.isOptionSet(Account.OPTION_REGISTER)) {
+ changeStatus(Account.State.REGISTRATION_NOT_SUPPORTED);
+ disconnect(true);
+ } else if (this.streamFeatures.hasChild("mechanisms")
+ && shouldAuthenticate
+ && (features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS)) {
+ authenticate();
+ } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" + smVersion) && streamId != null) {
+ if (Config.EXTENDED_SM_LOGGING) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": resuming after stanza #"+stanzasReceived);
+ }
+ final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived, smVersion);
+ this.tagWriter.writeStanzaAsync(resume);
+ } else if (needsBinding) {
+ if (this.streamFeatures.hasChild("bind")) {
+ sendBindRequest();
+ } else {
+ throw new IncompatibleServerException();
+ }
+ }
+ }
+
+ private void authenticate() throws IOException {
+ final List<String> mechanisms = extractMechanisms(streamFeatures
+ .findChild("mechanisms"));
+ final Element auth = new Element("auth");
+ auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
+ if (mechanisms.contains("EXTERNAL") && account.getPrivateKeyAlias() != null) {
+ saslMechanism = new External(tagWriter, account, ConversationsPlusApplication.getSecureRandom());
+ } else if (mechanisms.contains("SCRAM-SHA-1")) {
+ saslMechanism = new ScramSha1(tagWriter, account, ConversationsPlusApplication.getSecureRandom());
+ } else if (mechanisms.contains("PLAIN")) {
+ saslMechanism = new Plain(tagWriter, account);
+ } else if (mechanisms.contains("DIGEST-MD5")) {
+ saslMechanism = new DigestMd5(tagWriter, account, ConversationsPlusApplication.getSecureRandom());
+ }
+ if (saslMechanism != null) {
+ final JSONObject keys = account.getKeys();
+ try {
+ if (keys.has(Account.PINNED_MECHANISM_KEY) &&
+ keys.getInt(Account.PINNED_MECHANISM_KEY) > saslMechanism.getPriority()) {
+ Logging.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?");
+ throw new SecurityException();
+ }
+ } catch (final JSONException e) {
+ Logging.d(Config.LOGTAG, "Parse error while checking pinned auth mechanism");
+ }
+ Logging.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 {
+ throw new IncompatibleServerException();
+ }
+ }
+
+ private List<String> extractMechanisms(final Element stream) {
+ final ArrayList<String> mechanisms = new ArrayList<>(stream
+ .getChildren().size());
+ for (final Element child : stream.getChildren()) {
+ mechanisms.add(child.getContent());
+ }
+ return mechanisms;
+ }
+
+ public void sendCaptchaRegistryRequest(String id, Data data) {
+ if (data == null) {
+ setAccountCreationFailed("");
+ } else {
+ IqPacket request = getIqGenerator().generateCreateAccountWithCaptcha(account, id, data);
+ sendIqPacket(request, createPacketReceiveHandler());
+ }
+ }
+
+ private void sendRegistryRequest() {
+ final IqPacket register = new IqPacket(IqPacket.TYPE.GET);
+ register.query("jabber:iq:register");
+ register.setTo(account.getServer());
+ sendIqPacket(register, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(final Account account, final IqPacket packet) {
+ boolean failed = false;
+ if (packet.getType() == IqPacket.TYPE.RESULT
+ && packet.query().hasChild("username")
+ && (packet.query().hasChild("password"))) {
+ final IqPacket register = new IqPacket(IqPacket.TYPE.SET);
+ final Element username = new Element("username").setContent(account.getUsername());
+ final Element password = new Element("password").setContent(account.getPassword());
+ register.query("jabber:iq:register").addChild(username);
+ register.query().addChild(password);
+ sendIqPacket(register, createPacketReceiveHandler());
+ } else if (packet.getType() == IqPacket.TYPE.RESULT
+ && (packet.query().hasChild("x", "jabber:x:data"))) {
+ final Data data = Data.parse(packet.query().findChild("x", "jabber:x:data"));
+ final Element blob = packet.query().findChild("data", "urn:xmpp:bob");
+ final String id = packet.getId();
+
+ Bitmap captcha = null;
+ if (blob != null) {
+ try {
+ final String base64Blob = blob.getContent();
+ final byte[] strBlob = Base64.decode(base64Blob, Base64.DEFAULT);
+ InputStream stream = new ByteArrayInputStream(strBlob);
+ captcha = BitmapFactory.decodeStream(stream);
+ } catch (Exception e) {
+ //ignored
+ }
+ } else {
+ try {
+ Field url = data.getFieldByName("url");
+ String urlString = url.findChildContent("value");
+ URL uri = new URL(urlString);
+ captcha = BitmapFactory.decodeStream(uri.openConnection().getInputStream());
+ } catch (IOException e) {
+ Logging.e(Config.LOGTAG, e.toString());
+ }
+ }
+
+ if (captcha != null) {
+ failed = !mXmppConnectionService.displayCaptchaRequest(account, id, data, captcha);
+ }
+ } else {
+ failed = true;
+ }
+
+ if (failed) {
+ final Element instructions = packet.query().findChild("instructions");
+ setAccountCreationFailed((instructions != null) ? instructions.getContent() : "");
+ }
+ }
+ });
+ }
+
+ private void setAccountCreationFailed(String instructions) {
+ changeStatus(Account.State.REGISTRATION_FAILED);
+ disconnect(true);
+ Log.d(Config.LOGTAG, account.getJid().toBareJid()
+ + ": could not register. instructions are"
+ + instructions);
+ }
+
+ public void resetEverything() {
+ resetStreamId();
+ clearIqCallbacks();
+ mStanzaQueue.clear();
+ synchronized (this.disco) {
+ disco.clear();
+ }
+ }
+
+ private void sendBindRequest() {
+ while(!mXmppConnectionService.areMessagesInitialized() && socket != null && !socket.isClosed()) {
+ try {
+ Thread.sleep(500);
+ } catch (final InterruptedException ignored) {
+ }
+ }
+ needsBinding = false;
+ clearIqCallbacks();
+ final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
+ iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind")
+ .addChild("resource").setContent(account.getResource());
+ this.sendUnmodifiedIqPacket(iq, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(final Account account, final IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
+ return;
+ }
+ final Element bind = packet.findChild("bind");
+ if (bind != null && packet.getType() == IqPacket.TYPE.RESULT) {
+ final Element jid = bind.findChild("jid");
+ if (jid != null && jid.getContent() != null) {
+ 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("session")) {
+ sendStartSession();
+ } else {
+ sendPostBindInitialization();
+ }
+ } else {
+ Logging.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure. (no jid)");
+ disconnect(true);
+ }
+ } else {
+ Logging.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure ("+packet.toString());
+ disconnect(true);
+ }
+ }
+ });
+ }
+
+ private void clearIqCallbacks() {
+ final IqPacket failurePacket = new IqPacket(IqPacket.TYPE.TIMEOUT);
+ final ArrayList<OnIqPacketReceived> callbacks = new ArrayList<>();
+ synchronized (this.packetCallbacks) {
+ if (this.packetCallbacks.size() == 0) {
+ return;
+ }
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": clearing "+this.packetCallbacks.size()+" iq callbacks");
+ final Iterator<Pair<IqPacket, OnIqPacketReceived>> iterator = this.packetCallbacks.values().iterator();
+ while (iterator.hasNext()) {
+ Pair<IqPacket, OnIqPacketReceived> entry = iterator.next();
+ callbacks.add(entry.second);
+ iterator.remove();
+ }
+ }
+ for(OnIqPacketReceived callback : callbacks) {
+ callback.onIqPacketReceived(account,failurePacket);
+ }
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": done clearing iq callbacks. " + this.packetCallbacks.size() + " left");
+ }
+
+ public void sendDiscoTimeout() {
+ final IqPacket failurePacket = new IqPacket(IqPacket.TYPE.ERROR); //don't use timeout
+ final ArrayList<OnIqPacketReceived> callbacks = new ArrayList<>();
+ synchronized (this.mPendingServiceDiscoveriesIds) {
+ for(String id : mPendingServiceDiscoveriesIds) {
+ synchronized (this.packetCallbacks) {
+ Pair<IqPacket, OnIqPacketReceived> pair = this.packetCallbacks.remove(id);
+ if (pair != null) {
+ callbacks.add(pair.second);
+ }
+ }
+ }
+ this.mPendingServiceDiscoveriesIds.clear();
+ }
+ if (callbacks.size() > 0) {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": sending disco timeout");
+ resetStreamId(); //we don't want to live with this for ever
+ }
+ for(OnIqPacketReceived callback : callbacks) {
+ callback.onIqPacketReceived(account,failurePacket);
+ }
+ }
+
+ private void sendStartSession() {
+ final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET);
+ startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session");
+ this.sendUnmodifiedIqPacket(startSession, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ sendPostBindInitialization();
+ } else if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not init sessions");
+ disconnect(true);
+ }
+ }
+ });
+ }
+
+ private void sendPostBindInitialization() {
+ smVersion = 0;
+ if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) {
+ smVersion = 3;
+ } else if (streamFeatures.hasChild("sm", "urn:xmpp:sm:2")) {
+ smVersion = 2;
+ }
+ if (smVersion != 0) {
+ final EnablePacket enable = new EnablePacket(smVersion);
+ tagWriter.writeStanzaAsync(enable);
+ stanzasSent = 0;
+ mStanzaQueue.clear();
+ }
+ features.carbonsEnabled = false;
+ features.blockListRequested = false;
+ synchronized (this.disco) {
+ this.disco.clear();
+ }
+ mPendingServiceDiscoveries.set(0);
+ mIsServiceItemsDiscoveryPending.set(true);
+ mWaitForDisco = mServerIdentity != Identity.NIMBUZZ;
+ lastDiscoStarted = SystemClock.elapsedRealtime();
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": starting service discovery");
+ mXmppConnectionService.scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
+ Element caps = streamFeatures.findChild("c");
+ final String hash = caps == null ? null : caps.getAttribute("hash");
+ final String ver = caps == null ? null : caps.getAttribute("ver");
+ ServiceDiscoveryResult discoveryResult = null;
+ if (hash != null && ver != null) {
+ discoveryResult = mXmppConnectionService.databaseBackend.findDiscoveryResult(hash, ver);
+ }
+ if (discoveryResult == null) {
+ sendServiceDiscoveryInfo(account.getServer());
+ } else {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": server caps came from cache");
+ disco.put(account.getServer(), discoveryResult);
+ }
+ sendServiceDiscoveryInfo(account.getJid().toBareJid());
+ sendServiceDiscoveryItems(account.getServer());
+ if (!mWaitForDisco) {
+ finalizeBind();
+ }
+ this.lastSessionStarted = SystemClock.elapsedRealtime();
+ }
+
+ private void sendServiceDiscoveryInfo(final Jid jid) {
+ mPendingServiceDiscoveries.incrementAndGet();
+ final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
+ iq.setTo(jid);
+ iq.query("http://jabber.org/protocol/disco#info");
+ String id = this.sendIqPacket(iq, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(final Account account, final IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ boolean advancedStreamFeaturesLoaded;
+ synchronized (XmppConnection.this.disco) {
+ ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet);
+ for (final ServiceDiscoveryResult.Identity id : result.getIdentities()) {
+ if (mServerIdentity == Identity.UNKNOWN && id.getType().equals("im") &&
+ id.getCategory().equals("server") && id.getName() != null &&
+ jid.equals(account.getServer())) {
+ switch (id.getName()) {
+ case "Prosody":
+ mServerIdentity = Identity.PROSODY;
+ break;
+ case "ejabberd":
+ mServerIdentity = Identity.EJABBERD;
+ break;
+ case "Slack-XMPP":
+ mServerIdentity = Identity.SLACK;
+ break;
+ }
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": server name: " + id.getName());
+ }
+ }
+ if (jid.equals(account.getServer())) {
+ mXmppConnectionService.databaseBackend.insertDiscoveryResult(result);
+ }
+ disco.put(jid, result);
+ advancedStreamFeaturesLoaded = disco.containsKey(account.getServer())
+ && disco.containsKey(account.getJid().toBareJid());
+ }
+ if (advancedStreamFeaturesLoaded && (jid.equals(account.getServer()) || jid.equals(account.getJid().toBareJid()))) {
+ enableAdvancedStreamFeatures();
+ }
+ } else {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not query disco info for " + jid.toString());
+ }
+ if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
+ if (mPendingServiceDiscoveries.decrementAndGet() == 0
+ && !mIsServiceItemsDiscoveryPending.get()
+ && mWaitForDisco) {
+ finalizeBind();
+ }
+ }
+ }
+ });
+ synchronized (this.mPendingServiceDiscoveriesIds) {
+ this.mPendingServiceDiscoveriesIds.add(id);
+ }
+ }
+
+ private void finalizeBind() {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": online with resource " + account.getResource());
+ if (bindListener != null) {
+ bindListener.onBind(account);
+ }
+ changeStatus(Account.State.ONLINE);
+ }
+
+ private void enableAdvancedStreamFeatures() {
+ if (getFeatures().carbons() && !features.carbonsEnabled) {
+ sendEnableCarbons();
+ }
+ if (getFeatures().blocking() && !features.blockListRequested) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": Requesting block list");
+ this.sendIqPacket(getIqGenerator().generateGetBlockList(), mXmppConnectionService.getIqParser());
+ }
+ for (final OnAdvancedStreamFeaturesLoaded listener : advancedStreamFeaturesLoadedListeners) {
+ listener.onAdvancedStreamFeaturesAvailable(account);
+ }
+ }
+
+ 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");
+ String id = this.sendIqPacket(iq, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(final Account account, final IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ final List<Element> elements = packet.query().getChildren();
+ for (final Element element : elements) {
+ if (element.getName().equals("item")) {
+ final Jid jid = element.getAttributeAsJid("jid");
+ if (jid != null && !jid.equals(account.getServer())) {
+ sendServiceDiscoveryInfo(jid);
+ }
+ }
+ }
+ } else {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not query disco items of " + server);
+ }
+ if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
+ mIsServiceItemsDiscoveryPending.set(false);
+ if (mPendingServiceDiscoveries.get() == 0 && mWaitForDisco) {
+ finalizeBind();
+ }
+ }
+ }
+ });
+ synchronized (this.mPendingServiceDiscoveriesIds) {
+ this.mPendingServiceDiscoveriesIds.add(id);
+ }
+ }
+
+ private void sendEnableCarbons() {
+ final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
+ iq.addChild("enable", "urn:xmpp:carbons:2");
+ this.sendIqPacket(iq, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(final Account account, final IqPacket packet) {
+ if (!packet.hasChild("error")) {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid()
+ + ": successfully enabled carbons");
+ features.carbonsEnabled = true;
+ } else {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid()
+ + ": error enableing carbons " + packet.toString());
+ }
+ }
+ });
+ }
+
+ private void processStreamError(final Tag currentTag)
+ throws XmlPullParserException, IOException {
+ final Element streamError = tagReader.readElement(currentTag);
+ if (streamError != null && streamError.hasChild("conflict")) {
+ final String resource = account.getResource().split("\\.")[0];
+ account.setResource(resource + "." + nextRandomId());
+ Logging.d(Config.LOGTAG,
+ account.getJid().toBareJid() + ": switching resource due to conflict ("
+ + account.getResource() + ")");
+ } else if (streamError != null) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": stream error "+streamError.toString());
+ }
+ }
+
+ private void sendStartStream() throws IOException {
+ final Tag stream = Tag.start("stream:stream");
+ stream.setAttribute("to", account.getServer().toString());
+ stream.setAttribute("version", "1.0");
+ stream.setAttribute("xml:lang", "en");
+ stream.setAttribute("xmlns", "jabber:client");
+ stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
+ tagWriter.writeTag(stream);
+ }
+
+ private String nextRandomId() {
+ return new BigInteger(50, ConversationsPlusApplication.getSecureRandom()).toString(32);
+ }
+
+ public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) {
+ packet.setFrom(account.getJid());
+ return this.sendUnmodifiedIqPacket(packet, callback);
+ }
+
+ private synchronized String sendUnmodifiedIqPacket(final IqPacket packet, final OnIqPacketReceived callback) {
+ if (packet.getId() == null) {
+ final String id = nextRandomId();
+ packet.setAttribute("id", id);
+ }
+ if (callback != null) {
+ synchronized (this.packetCallbacks) {
+ packetCallbacks.put(packet.getId(), new Pair<>(packet, callback));
+ }
+ }
+ this.sendPacket(packet);
+ return packet.getId();
+ }
+
+ public void sendMessagePacket(final MessagePacket packet) {
+ this.sendPacket(packet);
+ }
+
+ public void sendPresencePacket(final PresencePacket packet) {
+ this.sendPacket(packet);
+ }
+
+ private synchronized void sendPacket(final AbstractStanza packet) {
+ if (stanzasSent == Integer.MAX_VALUE) {
+ resetStreamId();
+ disconnect(true);
+ return;
+ }
+ tagWriter.writeStanzaAsync(packet);
+ if (packet instanceof AbstractAcknowledgeableStanza) {
+ AbstractAcknowledgeableStanza stanza = (AbstractAcknowledgeableStanza) packet;
+ ++stanzasSent;
+ this.mStanzaQueue.put(stanzasSent, stanza);
+ if (stanza instanceof MessagePacket && stanza.getId() != null && getFeatures().sm()) {
+ if (Config.EXTENDED_SM_LOGGING) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": requesting ack for message stanza #" + stanzasSent);
+ }
+ tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion));
+ }
+ }
+ }
+
+ public void sendPing() {
+ if (!r()) {
+ final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
+ iq.setFrom(account.getJid());
+ iq.addChild("ping", "urn:xmpp:ping");
+ this.sendIqPacket(iq, null);
+ }
+ this.lastPingSent = SystemClock.elapsedRealtime();
+ }
+
+ public void setOnMessagePacketReceivedListener(
+ final OnMessagePacketReceived listener) {
+ this.messageListener = listener;
+ }
+
+ public void setOnUnregisteredIqPacketReceivedListener(
+ final OnIqPacketReceived listener) {
+ this.unregisteredIqListener = listener;
+ }
+
+ public void setOnPresencePacketReceivedListener(
+ final OnPresencePacketReceived listener) {
+ this.presenceListener = listener;
+ }
+
+ public void setOnJinglePacketReceivedListener(
+ final OnJinglePacketReceived listener) {
+ this.jingleListener = listener;
+ }
+
+ public void setOnStatusChangedListener(final OnStatusChanged listener) {
+ this.statusListener = listener;
+ }
+
+ public void setOnBindListener(final OnBindListener listener) {
+ this.bindListener = listener;
+ }
+
+ public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) {
+ this.acknowledgedListener = listener;
+ }
+
+ public void addOnAdvancedStreamFeaturesAvailableListener(final OnAdvancedStreamFeaturesLoaded listener) {
+ if (!this.advancedStreamFeaturesLoadedListeners.contains(listener)) {
+ this.advancedStreamFeaturesLoadedListeners.add(listener);
+ }
+ }
+
+ public void waitForPush() {
+ if (tagWriter.isActive()) {
+ tagWriter.finish();
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ while(!tagWriter.finished()) {
+ Thread.sleep(10);
+ }
+ socket.close();
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": closed tcp without closing stream");
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }).start();
+ } else {
+ forceCloseSocket();
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": closed tcp without closing stream (no waiting)");
+ }
+ }
+
+ private void forceCloseSocket() {
+ if (socket != null) {
+ try {
+ socket.close();
+ } catch (IOException e) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid().toString()+": exception during force close ("+e.getMessage()+")");
+ }
+ }
+ }
+
+ public void disconnect(final boolean force) {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": disconnecting force="+Boolean.valueOf(force));
+ if (force) {
+ forceCloseSocket();
+ return;
+ } else {
+ if (tagWriter.isActive()) {
+ tagWriter.finish();
+ try {
+ int i = 0;
+ boolean warned = false;
+ while (!tagWriter.finished() && socket.isConnected() && i <= 10) {
+ if (!warned) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid()+": waiting for tag writer to finish");
+ warned = true;
+ }
+ Thread.sleep(200);
+ i++;
+ }
+ if (warned) {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": tag writer has finished");
+ }
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": closing stream");
+ tagWriter.writeTag(Tag.end("stream:stream"));
+ } catch (final IOException e) {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": io exception during disconnect ("+e.getMessage()+")");
+ } catch (final InterruptedException e) {
+ Log.d(Config.LOGTAG, "interrupted");
+ }
+ }
+ }
+ }
+
+ public void resetStreamId() {
+ this.streamId = null;
+ }
+
+ private List<Entry<Jid, ServiceDiscoveryResult>> findDiscoItemsByFeature(final String feature) {
+ synchronized (this.disco) {
+ final List<Entry<Jid, ServiceDiscoveryResult>> items = new ArrayList<>();
+ for (final Entry<Jid, ServiceDiscoveryResult> cursor : this.disco.entrySet()) {
+ if (cursor.getValue().getFeatures().contains(feature)) {
+ items.add(cursor);
+ }
+ }
+ return items;
+ }
+ }
+
+ public Jid findDiscoItemByFeature(final String feature) {
+ final List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(feature);
+ if (items.size() >= 1) {
+ return items.get(0).getKey();
+ }
+ return null;
+ }
+
+ public boolean r() {
+ if (getFeatures().sm()) {
+ this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public String getMucServer() {
+ synchronized (this.disco) {
+ for (final Entry<Jid, ServiceDiscoveryResult> cursor : disco.entrySet()) {
+ final ServiceDiscoveryResult value = cursor.getValue();
+ if (value.getFeatures().contains("http://jabber.org/protocol/muc")
+ && !value.getFeatures().contains("jabber:iq:gateway")
+ && !value.hasIdentity("conference", "irc")) {
+ return cursor.getKey().toString();
+ }
+ }
+ }
+ return null;
+ }
+
+ public int getTimeToNextAttempt() {
+ final int interval = (int) (25 * Math.pow(1.5, attempt));
+ final int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
+ return interval - secondsSinceLast;
+ }
+
+ public int getAttempt() {
+ return this.attempt;
+ }
+
+ public Features getFeatures() {
+ return this.features;
+ }
+
+ public long getLastSessionEstablished() {
+ final long diff;
+ if (this.lastSessionStarted == 0) {
+ diff = SystemClock.elapsedRealtime() - this.lastConnect;
+ } else {
+ diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
+ }
+ return System.currentTimeMillis() - diff;
+ }
+
+ public long getLastConnect() {
+ return this.lastConnect;
+ }
+
+ public long getLastPingSent() {
+ return this.lastPingSent;
+ }
+
+ public long getLastDiscoStarted() {
+ return this.lastDiscoStarted;
+ }
+ public long getLastPacketReceived() {
+ return this.lastPacketReceived;
+ }
+
+ public void sendActive() {
+ this.sendPacket(new ActivePacket());
+ }
+
+ public void sendInactive() {
+ this.sendPacket(new InactivePacket());
+ }
+
+ public void resetAttemptCount() {
+ this.attempt = 0;
+ this.lastConnect = 0;
+ }
+
+ public void setInteractive(boolean interactive) {
+ this.mInteractive = interactive;
+ }
+
+ public Identity getServerIdentity() {
+ return mServerIdentity;
+ }
+
+ private class UnauthorizedException extends IOException {
+
+ }
+
+ private class SecurityException extends IOException {
+
+ }
+
+ private class IncompatibleServerException extends IOException {
+
+ }
+
+ public enum Identity {
+ FACEBOOK,
+ SLACK,
+ EJABBERD,
+ PROSODY,
+ NIMBUZZ,
+ UNKNOWN
+ }
+
+ public class Features {
+ XmppConnection connection;
+ private boolean carbonsEnabled = false;
+ private boolean encryptionEnabled = false;
+ private boolean blockListRequested = false;
+
+ public Features(final XmppConnection connection) {
+ this.connection = connection;
+ }
+
+ private boolean hasDiscoFeature(final Jid server, final String feature) {
+ synchronized (XmppConnection.this.disco) {
+ return connection.disco.containsKey(server) &&
+ connection.disco.get(server).getFeatures().contains(feature);
+ }
+ }
+
+ public boolean carbons() {
+ return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2");
+ }
+
+ public boolean blocking() {
+ return hasDiscoFeature(account.getServer(), Xmlns.BLOCKING);
+ }
+
+ public boolean register() {
+ return hasDiscoFeature(account.getServer(), Xmlns.REGISTER);
+ }
+
+ public boolean sm() {
+ return streamId != null
+ || (connection.streamFeatures != null && connection.streamFeatures.hasChild("sm"));
+ }
+
+ public boolean csi() {
+ return connection.streamFeatures != null && connection.streamFeatures.hasChild("csi", "urn:xmpp:csi:0");
+ }
+
+ public boolean pep() {
+ synchronized (XmppConnection.this.disco) {
+ ServiceDiscoveryResult info = disco.get(account.getJid().toBareJid());
+ return info != null && info.hasIdentity("pubsub", "pep");
+ }
+ }
+
+ public boolean pepPersistent() {
+ synchronized (XmppConnection.this.disco) {
+ ServiceDiscoveryResult info = disco.get(account.getJid().toBareJid());
+ return info != null && info.getFeatures().contains("http://jabber.org/protocol/pubsub#persistent-items");
+ }
+ }
+
+ public boolean mam() {
+ return hasDiscoFeature(account.getJid().toBareJid(), "urn:xmpp:mam:0")
+ || hasDiscoFeature(account.getServer(), "urn:xmpp:mam:0");
+ }
+
+ public boolean push() {
+ return hasDiscoFeature(account.getJid().toBareJid(), "urn:xmpp:push:0")
+ || hasDiscoFeature(account.getServer(), "urn:xmpp:push:0");
+ }
+
+ public boolean rosterVersioning() {
+ return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver");
+ }
+
+ public void setBlockListRequested(boolean value) {
+ this.blockListRequested = value;
+ }
+
+ public boolean httpUpload(long filesize) {
+ if (Config.DISABLE_HTTP_UPLOAD) {
+ return false;
+ } else {
+ List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(Xmlns.HTTP_UPLOAD);
+ if (items.size() > 0) {
+ try {
+ long maxsize = Long.parseLong(items.get(0).getValue().getExtendedDiscoInformation(Xmlns.HTTP_UPLOAD, "max-file-size"));
+ return filesize <= maxsize;
+ } catch (Exception e) {
+ return true;
+ }
+ } else {
+ return false;
+ }
+ }
+ }
+
+ public long getMaxHttpUploadSize() {
+ List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(Xmlns.HTTP_UPLOAD);
+ if (items.size() > 0) {
+ try {
+ return Long.parseLong(items.get(0).getValue().getExtendedDiscoInformation(Xmlns.HTTP_UPLOAD, "max-file-size"));
+ } catch (Exception e) {
+ return -1;
+ }
+ } else {
+ return -1;
+ }
+ }
+ }
+
+ private IqGenerator getIqGenerator() {
+ return mXmppConnectionService.getIqGenerator();
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/chatstate/ChatState.java b/src/main/java/eu/siacs/conversations/xmpp/chatstate/ChatState.java
new file mode 100644
index 00000000..3e371562
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/chatstate/ChatState.java
@@ -0,0 +1,32 @@
+package eu.siacs.conversations.xmpp.chatstate;
+
+import eu.siacs.conversations.xml.Element;
+
+public enum ChatState {
+
+ ACTIVE, INACTIVE, GONE, COMPOSING, PAUSED;
+
+ public static ChatState parse(Element element) {
+ final String NAMESPACE = "http://jabber.org/protocol/chatstates";
+ if (element.hasChild("active",NAMESPACE)) {
+ return ACTIVE;
+ } else if (element.hasChild("inactive",NAMESPACE)) {
+ return INACTIVE;
+ } else if (element.hasChild("composing",NAMESPACE)) {
+ return COMPOSING;
+ } else if (element.hasChild("gone",NAMESPACE)) {
+ return GONE;
+ } else if (element.hasChild("paused",NAMESPACE)) {
+ return PAUSED;
+ } else {
+ return null;
+ }
+ }
+
+ public static Element toElement(ChatState state) {
+ final String NAMESPACE = "http://jabber.org/protocol/chatstates";
+ final Element element = new Element(state.toString().toLowerCase());
+ element.setAttribute("xmlns",NAMESPACE);
+ return element;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java b/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java
new file mode 100644
index 00000000..8dabcb5b
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java
@@ -0,0 +1,99 @@
+package eu.siacs.conversations.xmpp.forms;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import eu.siacs.conversations.xml.Element;
+
+public class Data extends Element {
+
+ private static final String FORM_TYPE = "FORM_TYPE";
+
+ public Data() {
+ super("x");
+ this.setAttribute("xmlns","jabber:x:data");
+ }
+
+ public List<Field> getFields() {
+ ArrayList<Field> fields = new ArrayList<Field>();
+ for(Element child : getChildren()) {
+ if (child.getName().equals("field")
+ && !FORM_TYPE.equals(child.getAttribute("var"))) {
+ fields.add(Field.parse(child));
+ }
+ }
+ return fields;
+ }
+
+ public Field getFieldByName(String needle) {
+ for(Element child : getChildren()) {
+ if (child.getName().equals("field")
+ && needle.equals(child.getAttribute("var"))) {
+ return Field.parse(child);
+ }
+ }
+ return null;
+ }
+
+ public void put(String name, String value) {
+ Field field = getFieldByName(name);
+ if (field == null) {
+ field = new Field(name);
+ this.addChild(field);
+ }
+ field.setValue(value);
+ }
+
+ public void put(String name, Collection<String> values) {
+ Field field = getFieldByName(name);
+ if (field == null) {
+ field = new Field(name);
+ this.addChild(field);
+ }
+ field.setValues(values);
+ }
+
+ public void submit() {
+ this.setAttribute("type","submit");
+ removeUnnecessaryChildren();
+ for(Field field : getFields()) {
+ field.removeNonValueChildren();
+ }
+ }
+
+ private void removeUnnecessaryChildren() {
+ for(Iterator<Element> iterator = this.children.iterator(); iterator.hasNext();) {
+ Element element = iterator.next();
+ if (!element.getName().equals("field") && !element.getName().equals("title")) {
+ iterator.remove();
+ }
+ }
+ }
+
+ public static Data parse(Element element) {
+ Data data = new Data();
+ data.setAttributes(element.getAttributes());
+ data.setChildren(element.getChildren());
+ return data;
+ }
+
+ public void setFormType(String formType) {
+ this.put(FORM_TYPE, formType);
+ }
+
+ public String getFormType() {
+ String type = getValue(FORM_TYPE);
+ return type == null ? "" : type;
+ }
+
+ public String getValue(String name) {
+ Field field = this.getFieldByName(name);
+ return field == null ? null : field.getValue();
+ }
+
+ public String getTitle() {
+ return findChildContent("title");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/forms/Field.java b/src/main/java/eu/siacs/conversations/xmpp/forms/Field.java
new file mode 100644
index 00000000..020b34b9
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/forms/Field.java
@@ -0,0 +1,81 @@
+package eu.siacs.conversations.xmpp.forms;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import eu.siacs.conversations.xml.Element;
+
+public class Field extends Element {
+
+ public Field(String name) {
+ super("field");
+ this.setAttribute("var",name);
+ }
+
+ private Field() {
+ super("field");
+ }
+
+ public String getFieldName() {
+ return this.getAttribute("var");
+ }
+
+ public void setValue(String value) {
+ this.children.clear();
+ this.addChild("value").setContent(value);
+ }
+
+ public void setValues(Collection<String> values) {
+ this.children.clear();
+ for(String value : values) {
+ this.addChild("value").setContent(value);
+ }
+ }
+
+ public void removeNonValueChildren() {
+ for(Iterator<Element> iterator = this.children.iterator(); iterator.hasNext();) {
+ Element element = iterator.next();
+ if (!element.getName().equals("value")) {
+ iterator.remove();
+ }
+ }
+ }
+
+ public static Field parse(Element element) {
+ Field field = new Field();
+ field.setAttributes(element.getAttributes());
+ field.setChildren(element.getChildren());
+ return field;
+ }
+
+ public String getValue() {
+ return findChildContent("value");
+ }
+
+ public List<String> getValues() {
+ List<String> values = new ArrayList<>();
+ for(Element child : getChildren()) {
+ if ("value".equals(child.getName())) {
+ String content = child.getContent();
+ if (content != null) {
+ values.add(content);
+ }
+ }
+ }
+ return values;
+ }
+
+ public String getLabel() {
+ return getAttribute("label");
+ }
+
+ public String getType() {
+ return getAttribute("type");
+ }
+
+ public boolean isRequired() {
+ return hasChild("required");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jid/InvalidJidException.java b/src/main/java/eu/siacs/conversations/xmpp/jid/InvalidJidException.java
new file mode 100644
index 00000000..164e8849
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jid/InvalidJidException.java
@@ -0,0 +1,49 @@
+package eu.siacs.conversations.xmpp.jid;
+
+public class InvalidJidException extends Exception {
+
+ // This is probably not the "Java way", but the "Java way" means we'd have a ton of extra tiny,
+ // annoying classes floating around. I like this.
+ public final static String INVALID_LENGTH = "JID must be between 0 and 3071 characters";
+ public final static String INVALID_PART_LENGTH = "JID part must be between 0 and 1023 characters";
+ public final static String INVALID_CHARACTER = "JID contains an invalid character";
+ public final static String STRINGPREP_FAIL = "The STRINGPREP operation has failed for the given JID";
+ public final static String IS_NULL = "JID can not be NULL";
+
+ /**
+ * Constructs a new {@code Exception} that includes the current stack trace.
+ */
+ public InvalidJidException() {
+ }
+
+ /**
+ * Constructs a new {@code Exception} with the current stack trace and the
+ * specified detail message.
+ *
+ * @param detailMessage the detail message for this exception.
+ */
+ public InvalidJidException(final String detailMessage) {
+ super(detailMessage);
+ }
+
+ /**
+ * Constructs a new {@code Exception} with the current stack trace, the
+ * specified detail message and the specified cause.
+ *
+ * @param detailMessage the detail message for this exception.
+ * @param throwable the cause of this exception.
+ */
+ public InvalidJidException(final String detailMessage, final Throwable throwable) {
+ super(detailMessage, throwable);
+ }
+
+ /**
+ * Constructs a new {@code Exception} with the current stack trace and the
+ * specified cause.
+ *
+ * @param throwable the cause of this exception.
+ */
+ public InvalidJidException(final Throwable throwable) {
+ super(throwable);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java b/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java
new file mode 100644
index 00000000..a15abe14
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java
@@ -0,0 +1,226 @@
+package eu.siacs.conversations.xmpp.jid;
+
+import android.util.LruCache;
+
+import net.java.otr4j.session.SessionID;
+
+import java.net.IDN;
+
+import eu.siacs.conversations.Config;
+import gnu.inet.encoding.Stringprep;
+import gnu.inet.encoding.StringprepException;
+
+/**
+ * The `Jid' class provides an immutable representation of a JID.
+ */
+public final class Jid {
+
+ private static LruCache<String,Jid> cache = new LruCache<>(1024);
+
+ private final String localpart;
+ private final String domainpart;
+ private final String resourcepart;
+
+ // It's much more efficient to store the ful JID as well as the parts instead of figuring them
+ // all out every time (since some characters are displayed but aren't used for comparisons).
+ private final String displayjid;
+
+ public String getLocalpart() {
+ return localpart;
+ }
+
+ public String getDomainpart() {
+ return IDN.toUnicode(domainpart);
+ }
+
+ public String getResourcepart() {
+ return resourcepart;
+ }
+
+ public static Jid fromSessionID(final SessionID id) throws InvalidJidException{
+ if (id.getUserID().isEmpty()) {
+ return Jid.fromString(id.getAccountID());
+ } else {
+ return Jid.fromString(id.getAccountID()+"/"+id.getUserID());
+ }
+ }
+
+ public static Jid fromString(final String jid) throws InvalidJidException {
+ return Jid.fromString(jid, false);
+ }
+
+ public static Jid fromString(final String jid, final boolean safe) throws InvalidJidException {
+ return new Jid(jid, safe);
+ }
+
+ public static Jid fromParts(final String localpart,
+ final String domainpart,
+ final String resourcepart) throws InvalidJidException {
+ String out;
+ if (localpart == null || localpart.isEmpty()) {
+ out = domainpart;
+ } else {
+ out = localpart + "@" + domainpart;
+ }
+ if (resourcepart != null && !resourcepart.isEmpty()) {
+ out = out + "/" + resourcepart;
+ }
+ return new Jid(out, false);
+ }
+
+ private Jid(final String jid, final boolean safe) throws InvalidJidException {
+ if (jid == null) throw new InvalidJidException(InvalidJidException.IS_NULL);
+
+ Jid fromCache = Jid.cache.get(jid);
+ if (fromCache != null) {
+ displayjid = fromCache.displayjid;
+ localpart = fromCache.localpart;
+ domainpart = fromCache.domainpart;
+ resourcepart = fromCache.resourcepart;
+ return;
+ }
+
+ // Hackish Android way to count the number of chars in a string... should work everywhere.
+ final int atCount = jid.length() - jid.replace("@", "").length();
+ final int slashCount = jid.length() - jid.replace("/", "").length();
+
+ // Throw an error if there's anything obvious wrong with the JID...
+ if (jid.isEmpty() || jid.length() > 3071) {
+ throw new InvalidJidException(InvalidJidException.INVALID_LENGTH);
+ }
+
+ // Go ahead and check if the localpart or resourcepart is empty.
+ if (jid.startsWith("@") || (jid.endsWith("@") && slashCount == 0) || jid.startsWith("/") || (jid.endsWith("/") && slashCount < 2)) {
+ throw new InvalidJidException(InvalidJidException.INVALID_CHARACTER);
+ }
+
+ String finaljid;
+
+ final int domainpartStart;
+ final int atLoc = jid.indexOf("@");
+ final int slashLoc = jid.indexOf("/");
+ // If there is no "@" in the JID (eg. "example.net" or "example.net/resource")
+ // or there are one or more "@" signs but they're all in the resourcepart (eg. "example.net/@/rp@"):
+ if (atCount == 0 || (atCount > 0 && slashLoc != -1 && atLoc > slashLoc)) {
+ localpart = "";
+ finaljid = "";
+ domainpartStart = 0;
+ } else {
+ final String lp = jid.substring(0, atLoc);
+ try {
+ localpart = Config.DISABLE_STRING_PREP || safe ? lp : Stringprep.nodeprep(lp);
+ } catch (final StringprepException e) {
+ throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
+ }
+ if (localpart.isEmpty() || localpart.length() > 1023) {
+ throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH);
+ }
+ domainpartStart = atLoc + 1;
+ finaljid = lp + "@";
+ }
+
+ final String dp;
+ if (slashCount > 0) {
+ final String rp = jid.substring(slashLoc + 1, jid.length());
+ try {
+ resourcepart = Config.DISABLE_STRING_PREP || safe ? rp : Stringprep.resourceprep(rp);
+ } catch (final StringprepException e) {
+ throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
+ }
+ if (resourcepart.isEmpty() || resourcepart.length() > 1023) {
+ throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH);
+ }
+ try {
+ dp = IDN.toUnicode(Stringprep.nameprep(jid.substring(domainpartStart, slashLoc)), IDN.USE_STD3_ASCII_RULES);
+ } catch (final StringprepException e) {
+ throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
+ }
+ finaljid = finaljid + dp + "/" + rp;
+ } else {
+ resourcepart = "";
+ try{
+ dp = IDN.toUnicode(Stringprep.nameprep(jid.substring(domainpartStart, jid.length())), IDN.USE_STD3_ASCII_RULES);
+ } catch (final StringprepException e) {
+ throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
+ }
+ finaljid = finaljid + dp;
+ }
+
+ // Remove trailing "." before storing the domain part.
+ if (dp.endsWith(".")) {
+ try {
+ domainpart = IDN.toASCII(dp.substring(0, dp.length() - 1), IDN.USE_STD3_ASCII_RULES);
+ } catch (final IllegalArgumentException e) {
+ throw new InvalidJidException(e);
+ }
+ } else {
+ try {
+ domainpart = IDN.toASCII(dp, IDN.USE_STD3_ASCII_RULES);
+ } catch (final IllegalArgumentException e) {
+ throw new InvalidJidException(e);
+ }
+ }
+
+ // TODO: Find a proper domain validation library; validate individual parts, separators, etc.
+ if (domainpart.isEmpty() || domainpart.length() > 1023) {
+ throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH);
+ }
+
+ Jid.cache.put(jid, this);
+
+ this.displayjid = finaljid;
+ }
+
+ public Jid toBareJid() {
+ try {
+ return resourcepart.isEmpty() ? this : fromParts(localpart, domainpart, "");
+ } catch (final InvalidJidException e) {
+ // This should never happen.
+ throw new AssertionError("Jid " + this.toString() + " invalid");
+ }
+ }
+
+ public Jid toDomainJid() {
+ try {
+ return resourcepart.isEmpty() && localpart.isEmpty() ? this : fromString(getDomainpart());
+ } catch (final InvalidJidException e) {
+ // This should never happen.
+ throw new AssertionError("Jid " + this.toString() + " invalid");
+ }
+ }
+
+ @Override
+ public String toString() {
+ return displayjid;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ final Jid jid = (Jid) o;
+
+ return jid.hashCode() == this.hashCode();
+ }
+
+ @Override
+ public int hashCode() {
+ int result = localpart.hashCode();
+ result = 31 * result + domainpart.hashCode();
+ result = 31 * result + resourcepart.hashCode();
+ return result;
+ }
+
+ public boolean hasLocalpart() {
+ return !localpart.isEmpty();
+ }
+
+ public boolean isBareJid() {
+ return this.resourcepart.isEmpty();
+ }
+
+ public boolean isDomainJid() {
+ return !this.hasLocalpart();
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java
new file mode 100644
index 00000000..dcadb92f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java
@@ -0,0 +1,147 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class JingleCandidate {
+
+ public static int TYPE_UNKNOWN;
+ public static int TYPE_DIRECT = 0;
+ public static int TYPE_PROXY = 1;
+
+ private boolean ours;
+ private boolean usedByCounterpart = false;
+ private String cid;
+ private String host;
+ private int port;
+ private int type;
+ private Jid jid;
+ private int priority;
+
+ public JingleCandidate(String cid, boolean ours) {
+ this.ours = ours;
+ this.cid = cid;
+ }
+
+ public String getCid() {
+ return cid;
+ }
+
+ public void setHost(String host) {
+ this.host = host;
+ }
+
+ public String getHost() {
+ return this.host;
+ }
+
+ public void setJid(final Jid jid) {
+ this.jid = jid;
+ }
+
+ public Jid getJid() {
+ return this.jid;
+ }
+
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ public int getPort() {
+ return this.port;
+ }
+
+ public void setType(int type) {
+ this.type = type;
+ }
+
+ public void setType(String type) {
+ switch (type) {
+ case "proxy":
+ this.type = TYPE_PROXY;
+ break;
+ case "direct":
+ this.type = TYPE_DIRECT;
+ break;
+ default:
+ this.type = TYPE_UNKNOWN;
+ break;
+ }
+ }
+
+ public void setPriority(int i) {
+ this.priority = i;
+ }
+
+ public int getPriority() {
+ return this.priority;
+ }
+
+ public boolean equals(JingleCandidate other) {
+ return this.getCid().equals(other.getCid());
+ }
+
+ public boolean equalValues(JingleCandidate other) {
+ return other != null && other.getHost().equals(this.getHost()) && (other.getPort() == this.getPort());
+ }
+
+ public boolean isOurs() {
+ return ours;
+ }
+
+ public int getType() {
+ return this.type;
+ }
+
+ public static List<JingleCandidate> parse(List<Element> canditates) {
+ List<JingleCandidate> parsedCandidates = new ArrayList<>();
+ for (Element c : canditates) {
+ parsedCandidates.add(JingleCandidate.parse(c));
+ }
+ return parsedCandidates;
+ }
+
+ public static JingleCandidate parse(Element candidate) {
+ JingleCandidate parsedCandidate = new JingleCandidate(
+ candidate.getAttribute("cid"), false);
+ parsedCandidate.setHost(candidate.getAttribute("host"));
+ parsedCandidate.setJid(candidate.getAttributeAsJid("jid"));
+ parsedCandidate.setType(candidate.getAttribute("type"));
+ parsedCandidate.setPriority(Integer.parseInt(candidate
+ .getAttribute("priority")));
+ parsedCandidate
+ .setPort(Integer.parseInt(candidate.getAttribute("port")));
+ return parsedCandidate;
+ }
+
+ public Element toElement() {
+ Element element = new Element("candidate");
+ element.setAttribute("cid", this.getCid());
+ element.setAttribute("host", this.getHost());
+ element.setAttribute("port", Integer.toString(this.getPort()));
+ element.setAttribute("jid", this.getJid().toString());
+ element.setAttribute("priority", Integer.toString(this.getPriority()));
+ if (this.getType() == TYPE_DIRECT) {
+ element.setAttribute("type", "direct");
+ } else if (this.getType() == TYPE_PROXY) {
+ element.setAttribute("type", "proxy");
+ }
+ return element;
+ }
+
+ public void flagAsUsedByCounterpart() {
+ this.usedByCounterpart = true;
+ }
+
+ public boolean isUsedByCounterpart() {
+ return this.usedByCounterpart;
+ }
+
+ public String toString() {
+ return this.getHost() + ":" + this.getPort() + " (prio="
+ + this.getPriority() + ")";
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
new file mode 100644
index 00000000..fca0597f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
@@ -0,0 +1,1025 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import android.util.Pair;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+import de.thedevstack.conversationsplus.utils.StreamUtil;
+import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.OnMessageCreatedCallback;
+import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.entities.TransferablePlaceholder;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.services.AbstractConnectionManager;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.FileUtils;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
+import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class JingleConnection implements Transferable {
+
+ private JingleConnectionManager mJingleConnectionManager;
+ private XmppConnectionService mXmppConnectionService;
+
+ protected static final int JINGLE_STATUS_INITIATED = 0;
+ protected static final int JINGLE_STATUS_ACCEPTED = 1;
+ protected static final int JINGLE_STATUS_FINISHED = 4;
+ protected static final int JINGLE_STATUS_TRANSMITTING = 5;
+ protected static final int JINGLE_STATUS_FAILED = 99;
+
+ private int ibbBlockSize = 8192;
+
+ private int mJingleStatus = -1;
+ private int mStatus = Transferable.STATUS_UNKNOWN;
+ private Message message;
+ private String sessionId;
+ private Account account;
+ private Jid initiator;
+ private Jid responder;
+ private List<JingleCandidate> candidates = new ArrayList<>();
+ private ConcurrentHashMap<String, JingleSocks5Transport> connections = new ConcurrentHashMap<>();
+
+ private String transportId;
+ private Element fileOffer;
+ private DownloadableFile file = null;
+
+ private String contentName;
+ private String contentCreator;
+
+ private int mProgress = 0;
+
+ private boolean receivedCandidate = false;
+ private boolean sentCandidate = false;
+
+ private boolean acceptedAutomatically = false;
+
+ private XmppAxolotlMessage mXmppAxolotlMessage;
+
+ private JingleTransport transport = null;
+
+ private OutputStream mFileOutputStream;
+ private InputStream mFileInputStream;
+
+ private OnIqPacketReceived responseListener = new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() != IqPacket.TYPE.RESULT) {
+ fail();
+ }
+ }
+ };
+
+ final OnFileTransmissionStatusChanged onFileTransmissionSatusChanged = new OnFileTransmissionStatusChanged() {
+
+ @Override
+ public void onFileTransmitted(DownloadableFile file) {
+ if (responder.equals(account.getJid())) {
+ sendSuccess();
+ MessageUtil.updateFileParams(message);
+ mXmppConnectionService.databaseBackend.createMessage(message);
+ MessageUtil.markMessage(message,Message.STATUS_RECEIVED);
+ if (acceptedAutomatically) {
+ message.markUnread();
+ JingleConnection.this.mXmppConnectionService.getNotificationService().push(message);
+ }
+ } else {
+ if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+ file.delete();
+ }
+ }
+ Logging.d(Config.LOGTAG,"successfully transmitted file:" + file.getAbsolutePath()+" ("+file.getSha1Sum()+")");
+ if (message.getEncryption() != Message.ENCRYPTION_PGP) {
+ FileBackend.updateMediaScanner(file, mXmppConnectionService);
+ } else {
+ account.getPgpDecryptionService().add(message);
+ }
+ }
+
+ @Override
+ public void onFileTransferAborted() {
+ JingleConnection.this.sendCancel();
+ JingleConnection.this.fail();
+ }
+ };
+
+ public InputStream getFileInputStream() {
+ return this.mFileInputStream;
+ }
+
+ public OutputStream getFileOutputStream() {
+ return this.mFileOutputStream;
+ }
+
+ private OnProxyActivated onProxyActivated = new OnProxyActivated() {
+
+ @Override
+ public void success() {
+ if (initiator.equals(account.getJid())) {
+ Logging.d(Config.LOGTAG, "we were initiating. sending file");
+ transport.send(file, onFileTransmissionSatusChanged);
+ } else {
+ transport.receive(file, onFileTransmissionSatusChanged);
+ Logging.d(Config.LOGTAG, "we were responding. receiving file");
+ }
+ }
+
+ @Override
+ public void failed() {
+ Logging.d(Config.LOGTAG, "proxy activation failed");
+ }
+ };
+
+ public JingleConnection(JingleConnectionManager mJingleConnectionManager) {
+ this.mJingleConnectionManager = mJingleConnectionManager;
+ this.mXmppConnectionService = XmppConnectionServiceAccessor.xmppConnectionService;
+ }
+
+ public String getSessionId() {
+ return this.sessionId;
+ }
+
+ public Account getAccount() {
+ return this.account;
+ }
+
+ public Jid getCounterPart() {
+ return this.message.getCounterpart();
+ }
+
+ public void deliverPacket(JinglePacket packet) {
+ boolean returnResult = true;
+ if (packet.isAction("session-terminate")) {
+ Reason reason = packet.getReason();
+ if (reason != null) {
+ if (reason.hasChild("cancel")) {
+ this.fail();
+ } else if (reason.hasChild("success")) {
+ this.receiveSuccess();
+ } else {
+ this.fail();
+ }
+ } else {
+ this.fail();
+ }
+ } else if (packet.isAction("session-accept")) {
+ returnResult = receiveAccept(packet);
+ } else if (packet.isAction("transport-info")) {
+ returnResult = receiveTransportInfo(packet);
+ } else if (packet.isAction("transport-replace")) {
+ if (packet.getJingleContent().hasIbbTransport()) {
+ returnResult = this.receiveFallbackToIbb(packet);
+ } else {
+ returnResult = false;
+ Logging.d(Config.LOGTAG, "trying to fallback to something unknown"
+ + packet.toString());
+ }
+ } else if (packet.isAction("transport-accept")) {
+ returnResult = this.receiveTransportAccept(packet);
+ } else {
+ Logging.d(Config.LOGTAG, "packet arrived in connection. action was "
+ + packet.getAction());
+ returnResult = false;
+ }
+ IqPacket response;
+ if (returnResult) {
+ response = packet.generateResponse(IqPacket.TYPE.RESULT);
+
+ } else {
+ response = packet.generateResponse(IqPacket.TYPE.ERROR);
+ }
+ mXmppConnectionService.sendIqPacket(account,response,null);
+ }
+
+ public void init(final Message message) {
+ if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
+ Conversation conversation = message.getConversation();
+ conversation.getAccount().getAxolotlService().prepareKeyTransportMessage(conversation, new OnMessageCreatedCallback() {
+ @Override
+ public void run(XmppAxolotlMessage xmppAxolotlMessage) {
+ if (xmppAxolotlMessage != null) {
+ init(message, xmppAxolotlMessage);
+ } else {
+ fail();
+ }
+ }
+ });
+ } else {
+ init(message, null);
+ }
+ }
+
+ private void init(Message message, XmppAxolotlMessage xmppAxolotlMessage) {
+ this.mXmppAxolotlMessage = xmppAxolotlMessage;
+ this.contentCreator = "initiator";
+ this.contentName = this.mJingleConnectionManager.nextRandomId();
+ this.message = message;
+ this.message.setTransferable(this);
+ this.mStatus = Transferable.STATUS_UPLOADING;
+ this.account = message.getConversation().getAccount();
+ this.initiator = this.account.getJid();
+ this.responder = this.message.getCounterpart();
+ this.sessionId = this.mJingleConnectionManager.nextRandomId();
+ if (this.candidates.size() > 0) {
+ this.sendInitRequest();
+ } else {
+ this.mJingleConnectionManager.getPrimaryCandidate(account,
+ new OnPrimaryCandidateFound() {
+
+ @Override
+ public void onPrimaryCandidateFound(boolean success,
+ final JingleCandidate candidate) {
+ if (success) {
+ final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
+ JingleConnection.this, candidate);
+ connections.put(candidate.getCid(),
+ socksConnection);
+ socksConnection
+ .connect(new OnTransportConnected() {
+
+ @Override
+ public void failed() {
+ Logging.d(Config.LOGTAG,
+ "connection to our own primary candidete failed");
+ sendInitRequest();
+ }
+
+ @Override
+ public void established() {
+ Logging.d(Config.LOGTAG,
+ "succesfully connected to our own primary candidate");
+ mergeCandidate(candidate);
+ sendInitRequest();
+ }
+ });
+ mergeCandidate(candidate);
+ } else {
+ Logging.d(Config.LOGTAG,
+ "no primary candidate of our own was found");
+ sendInitRequest();
+ }
+ }
+ });
+ }
+
+ }
+
+ public void init(Account account, JinglePacket packet) {
+ this.mJingleStatus = JINGLE_STATUS_INITIATED;
+ Conversation conversation = this.mXmppConnectionService
+ .findOrCreateConversation(account,
+ packet.getFrom().toBareJid(), false);
+ this.message = new Message(conversation, "", Message.ENCRYPTION_NONE);
+ this.message.setStatus(Message.STATUS_RECEIVED);
+ this.mStatus = Transferable.STATUS_OFFER;
+ this.message.setTransferable(this);
+ final Jid from = packet.getFrom();
+ this.message.setCounterpart(from);
+ this.account = account;
+ this.initiator = packet.getFrom();
+ this.responder = this.account.getJid();
+ this.sessionId = packet.getSessionId();
+ Content content = packet.getJingleContent();
+ this.contentCreator = content.getAttribute("creator");
+ this.contentName = content.getAttribute("name");
+ this.transportId = content.getTransportId();
+ this.mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren()));
+ this.fileOffer = packet.getJingleContent().getFileOffer();
+
+ mXmppConnectionService.sendIqPacket(account,packet.generateResponse(IqPacket.TYPE.RESULT),null);
+
+ if (fileOffer != null) {
+ Element encrypted = fileOffer.findChild("encrypted", AxolotlService.PEP_PREFIX);
+ if (encrypted != null) {
+ this.mXmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, packet.getFrom().toBareJid());
+ }
+ Element fileSize = fileOffer.findChild("size");
+ Element fileNameElement = fileOffer.findChild("name");
+ if (fileNameElement != null) {
+ String filename = fileNameElement.getContent()
+ .toLowerCase(Locale.US).toLowerCase();
+ final String lastPart = FileUtils.getLastExtension(filename);
+ final String secondToLastPart = FileUtils.getSecondToLastExtension(filename);
+ if (!lastPart.isEmpty()) {
+ if (VALID_IMAGE_EXTENSIONS.contains(lastPart)) {
+ message.setType(Message.TYPE_IMAGE);
+ message.setRelativeFilePath(message.getUuid()+"."+lastPart);
+ } else if (VALID_CRYPTO_EXTENSIONS.contains(lastPart)) {
+ if (!secondToLastPart.isEmpty()) {
+ if (VALID_IMAGE_EXTENSIONS.contains(secondToLastPart)) {
+ message.setType(Message.TYPE_IMAGE);
+ message.setRelativeFilePath(message.getUuid()+"."+secondToLastPart);
+ } else {
+ message.setType(Message.TYPE_FILE);
+ message.setRelativeFilePath(message.getUuid() + "_"
+ + filename.substring(0, filename.length() - (secondToLastPart.length() + 1)));
+ }
+ if (lastPart.equals("otr")) {
+ message.setEncryption(Message.ENCRYPTION_OTR);
+ } else {
+ message.setEncryption(Message.ENCRYPTION_PGP);
+ }
+ }
+ } else {
+ message.setType(Message.TYPE_FILE);
+ message.setRelativeFilePath(message.getUuid() + "_" + filename);
+ }
+ } else {
+ message.setType(Message.TYPE_FILE);
+ message.setRelativeFilePath(message.getUuid() + "_" + filename);
+ }
+
+ long size = Long.parseLong(fileSize.getContent());
+ message.setBody(Long.toString(size));
+ conversation.add(message);
+ mXmppConnectionService.updateConversationUi();
+ if (mJingleConnectionManager.hasStoragePermission()
+ && size <= ConversationsPlusPreferences.autoAcceptFileSize()
+ && mXmppConnectionService.isDownloadAllowedInConnection()) {
+ Logging.d(Config.LOGTAG, "auto accepting file from "+ packet.getFrom());
+ this.acceptedAutomatically = true;
+ this.sendAccept();
+ } else {
+ message.markUnread();
+ Logging.d(Config.LOGTAG,
+ "not auto accepting new file offer with size: "
+ + size
+ + " allowed size:"
+ + ConversationsPlusPreferences.autoAcceptFileSize());
+ this.mXmppConnectionService.getNotificationService().push(message);
+ }
+ this.file = FileBackend.getFile(message, false);
+ if (mXmppAxolotlMessage != null) {
+ XmppAxolotlMessage.XmppAxolotlKeyTransportMessage transportMessage = account.getAxolotlService().processReceivingKeyTransportMessage(mXmppAxolotlMessage);
+ if (transportMessage != null) {
+ message.setEncryption(Message.ENCRYPTION_AXOLOTL);
+ this.file.setKey(transportMessage.getKey());
+ this.file.setIv(transportMessage.getIv());
+ message.setFingerprint(transportMessage.getFingerprint());
+ } else {
+ Logging.d(Config.LOGTAG,"could not process KeyTransportMessage");
+ }
+ } else if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ byte[] key = conversation.getSymmetricKey();
+ if (key == null) {
+ this.sendCancel();
+ this.fail();
+ return;
+ } else {
+ this.file.setKeyAndIv(key);
+ }
+ }
+ this.mFileOutputStream = AbstractConnectionManager.createOutputStream(this.file,message.getEncryption() == Message.ENCRYPTION_AXOLOTL);
+ if (message.getEncryption() == Message.ENCRYPTION_OTR && Config.REPORT_WRONG_FILESIZE_IN_OTR_JINGLE) {
+ this.file.setExpectedSize((size / 16 + 1) * 16);
+ } else {
+ this.file.setExpectedSize(size);
+ }
+ Logging.d(Config.LOGTAG, "receiving file: expecting size of " + this.file.getExpectedSize());
+ } else {
+ this.sendCancel();
+ this.fail();
+ }
+ } else {
+ this.sendCancel();
+ this.fail();
+ }
+ }
+
+ private void sendInitRequest() {
+ JinglePacket packet = this.bootstrapPacket("session-initiate");
+ Content content = new Content(this.contentCreator, this.contentName);
+ if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
+ content.setTransportId(this.transportId);
+ this.file = FileBackend.getFile(message, false);
+ Pair<InputStream,Integer> pair;
+ try {
+ if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ Conversation conversation = this.message.getConversation();
+ if (!this.mXmppConnectionService.renewSymmetricKey(conversation)) {
+ Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not set symmetric key");
+ cancel();
+ }
+ this.file.setKeyAndIv(conversation.getSymmetricKey());
+ pair = AbstractConnectionManager.createInputStream(this.file, false);
+ this.file.setExpectedSize(pair.second);
+ content.setFileOffer(this.file, true);
+ } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
+ this.file.setKey(mXmppAxolotlMessage.getInnerKey());
+ this.file.setIv(mXmppAxolotlMessage.getIV());
+ pair = AbstractConnectionManager.createInputStream(this.file, true);
+ this.file.setExpectedSize(pair.second);
+ content.setFileOffer(this.file, false).addChild(mXmppAxolotlMessage.toElement());
+ } else {
+ pair = AbstractConnectionManager.createInputStream(this.file, false);
+ this.file.setExpectedSize(pair.second);
+ content.setFileOffer(this.file, false);
+ }
+ } catch (FileNotFoundException e) {
+ cancel();
+ return;
+ }
+ this.mFileInputStream = pair.first;
+ this.transportId = this.mJingleConnectionManager.nextRandomId();
+ content.setTransportId(this.transportId);
+ content.socks5transport().setChildren(getCandidatesAsElements());
+ packet.setContent(content);
+ this.sendJinglePacket(packet,new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": other party received offer");
+ mJingleStatus = JINGLE_STATUS_INITIATED;
+ MessageUtil.markMessage(message, Message.STATUS_OFFERED);
+ } else {
+ fail();
+ }
+ }
+ });
+
+ }
+ }
+
+ private List<Element> getCandidatesAsElements() {
+ List<Element> elements = new ArrayList<>();
+ for (JingleCandidate c : this.candidates) {
+ if (c.isOurs()) {
+ elements.add(c.toElement());
+ }
+ }
+ return elements;
+ }
+
+ private void sendAccept() {
+ mJingleStatus = JINGLE_STATUS_ACCEPTED;
+ this.mStatus = Transferable.STATUS_DOWNLOADING;
+ mXmppConnectionService.updateConversationUi();
+ this.mJingleConnectionManager.getPrimaryCandidate(this.account, new OnPrimaryCandidateFound() {
+ @Override
+ public void onPrimaryCandidateFound(boolean success, final JingleCandidate candidate) {
+ final JinglePacket packet = bootstrapPacket("session-accept");
+ final Content content = new Content(contentCreator,contentName);
+ content.setFileOffer(fileOffer);
+ content.setTransportId(transportId);
+ if (success && candidate != null && !equalCandidateExists(candidate)) {
+ final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
+ JingleConnection.this,
+ candidate);
+ connections.put(candidate.getCid(), socksConnection);
+ socksConnection.connect(new OnTransportConnected() {
+
+ @Override
+ public void failed() {
+ Logging.d(Config.LOGTAG,"connection to our own primary candidate failed");
+ content.socks5transport().setChildren(getCandidatesAsElements());
+ packet.setContent(content);
+ sendJinglePacket(packet);
+ connectNextCandidate();
+ }
+
+ @Override
+ public void established() {
+ Logging.d(Config.LOGTAG, "connected to primary candidate");
+ mergeCandidate(candidate);
+ content.socks5transport().setChildren(getCandidatesAsElements());
+ packet.setContent(content);
+ sendJinglePacket(packet);
+ connectNextCandidate();
+ }
+ });
+ } else {
+ Logging.d(Config.LOGTAG,"did not find a primary candidate for ourself");
+ content.socks5transport().setChildren(getCandidatesAsElements());
+ packet.setContent(content);
+ sendJinglePacket(packet);
+ connectNextCandidate();
+ }
+ }
+ });
+ }
+
+ private JinglePacket bootstrapPacket(String action) {
+ JinglePacket packet = new JinglePacket();
+ packet.setAction(action);
+ packet.setFrom(account.getJid());
+ packet.setTo(this.message.getCounterpart());
+ packet.setSessionId(this.sessionId);
+ packet.setInitiator(this.initiator);
+ return packet;
+ }
+
+ private void sendJinglePacket(JinglePacket packet) {
+ mXmppConnectionService.sendIqPacket(account,packet,responseListener);
+ }
+
+ private void sendJinglePacket(JinglePacket packet, OnIqPacketReceived callback) {
+ mXmppConnectionService.sendIqPacket(account,packet,callback);
+ }
+
+ private boolean receiveAccept(JinglePacket packet) {
+ Content content = packet.getJingleContent();
+ mergeCandidates(JingleCandidate.parse(content.socks5transport()
+ .getChildren()));
+ this.mJingleStatus = JINGLE_STATUS_ACCEPTED;
+ MessageUtil.markMessage(message, Message.STATUS_UNSEND);
+ this.connectNextCandidate();
+ return true;
+ }
+
+ private boolean receiveTransportInfo(JinglePacket packet) {
+ Content content = packet.getJingleContent();
+ if (content.hasSocks5Transport()) {
+ if (content.socks5transport().hasChild("activated")) {
+ if ((this.transport != null) && (this.transport instanceof JingleSocks5Transport)) {
+ onProxyActivated.success();
+ } else {
+ String cid = content.socks5transport().findChild("activated").getAttribute("cid");
+ Logging.d(Config.LOGTAG, "received proxy activated (" + cid
+ + ")prior to choosing our own transport");
+ JingleSocks5Transport connection = this.connections.get(cid);
+ if (connection != null) {
+ connection.setActivated(true);
+ } else {
+ Logging.d(Config.LOGTAG, "activated connection not found");
+ this.sendCancel();
+ this.fail();
+ }
+ }
+ return true;
+ } else if (content.socks5transport().hasChild("proxy-error")) {
+ onProxyActivated.failed();
+ return true;
+ } else if (content.socks5transport().hasChild("candidate-error")) {
+ Logging.d(Config.LOGTAG, "received candidate error");
+ this.receivedCandidate = true;
+ if ((mJingleStatus == JINGLE_STATUS_ACCEPTED)
+ && (this.sentCandidate)) {
+ this.connect();
+ }
+ return true;
+ } else if (content.socks5transport().hasChild("candidate-used")) {
+ String cid = content.socks5transport()
+ .findChild("candidate-used").getAttribute("cid");
+ if (cid != null) {
+ Logging.d(Config.LOGTAG, "candidate used by counterpart:" + cid);
+ JingleCandidate candidate = getCandidate(cid);
+ candidate.flagAsUsedByCounterpart();
+ this.receivedCandidate = true;
+ if ((mJingleStatus == JINGLE_STATUS_ACCEPTED)
+ && (this.sentCandidate)) {
+ this.connect();
+ } else {
+ Logging.d(Config.LOGTAG,
+ "ignoring because file is already in transmission or we havent sent our candidate yet");
+ }
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ } else {
+ return true;
+ }
+ }
+
+ private void connect() {
+ final JingleSocks5Transport connection = chooseConnection();
+ this.transport = connection;
+ if (connection == null) {
+ Logging.d(Config.LOGTAG, "could not find suitable candidate");
+ this.disconnectSocks5Connections();
+ if (this.initiator.equals(account.getJid())) {
+ this.sendFallbackToIbb();
+ }
+ } else {
+ this.mJingleStatus = JINGLE_STATUS_TRANSMITTING;
+ if (connection.needsActivation()) {
+ if (connection.getCandidate().isOurs()) {
+ Logging.d(Config.LOGTAG, "candidate "
+ + connection.getCandidate().getCid()
+ + " was our proxy. going to activate");
+ IqPacket activation = new IqPacket(IqPacket.TYPE.SET);
+ activation.setTo(connection.getCandidate().getJid());
+ activation.query("http://jabber.org/protocol/bytestreams")
+ .setAttribute("sid", this.getSessionId());
+ activation.query().addChild("activate")
+ .setContent(this.getCounterPart().toString());
+ mXmppConnectionService.sendIqPacket(account,activation,
+ new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account,
+ IqPacket packet) {
+ if (packet.getType() != IqPacket.TYPE.RESULT) {
+ onProxyActivated.failed();
+ } else {
+ onProxyActivated.success();
+ sendProxyActivated(connection.getCandidate().getCid());
+ }
+ }
+ });
+ } else {
+ Logging.d(Config.LOGTAG,
+ "candidate "
+ + connection.getCandidate().getCid()
+ + " was a proxy. waiting for other party to activate");
+ }
+ } else {
+ if (initiator.equals(account.getJid())) {
+ Logging.d(Config.LOGTAG, "we were initiating. sending file");
+ connection.send(file, onFileTransmissionSatusChanged);
+ } else {
+ Logging.d(Config.LOGTAG, "we were responding. receiving file");
+ connection.receive(file, onFileTransmissionSatusChanged);
+ }
+ }
+ }
+ }
+
+ private JingleSocks5Transport chooseConnection() {
+ JingleSocks5Transport connection = null;
+ for (Entry<String, JingleSocks5Transport> cursor : connections
+ .entrySet()) {
+ JingleSocks5Transport currentConnection = cursor.getValue();
+ // Logging.d(Config.LOGTAG,"comparing candidate: "+currentConnection.getCandidate().toString());
+ if (currentConnection.isEstablished()
+ && (currentConnection.getCandidate().isUsedByCounterpart() || (!currentConnection
+ .getCandidate().isOurs()))) {
+ // Logging.d(Config.LOGTAG,"is usable");
+ if (connection == null) {
+ connection = currentConnection;
+ } else {
+ if (connection.getCandidate().getPriority() < currentConnection
+ .getCandidate().getPriority()) {
+ connection = currentConnection;
+ } else if (connection.getCandidate().getPriority() == currentConnection
+ .getCandidate().getPriority()) {
+ // Logging.d(Config.LOGTAG,"found two candidates with same priority");
+ if (initiator.equals(account.getJid())) {
+ if (currentConnection.getCandidate().isOurs()) {
+ connection = currentConnection;
+ }
+ } else {
+ if (!currentConnection.getCandidate().isOurs()) {
+ connection = currentConnection;
+ }
+ }
+ }
+ }
+ }
+ }
+ return connection;
+ }
+
+ private void sendSuccess() {
+ JinglePacket packet = bootstrapPacket("session-terminate");
+ Reason reason = new Reason();
+ reason.addChild("success");
+ packet.setReason(reason);
+ this.sendJinglePacket(packet);
+ this.disconnectSocks5Connections();
+ this.mJingleStatus = JINGLE_STATUS_FINISHED;
+ this.message.setStatus(Message.STATUS_RECEIVED);
+ this.message.setTransferable(null);
+ this.mXmppConnectionService.updateMessage(message);
+ this.mJingleConnectionManager.finishConnection(this);
+ }
+
+ private void sendFallbackToIbb() {
+ Logging.d(Config.LOGTAG, "sending fallback to ibb");
+ JinglePacket packet = this.bootstrapPacket("transport-replace");
+ Content content = new Content(this.contentCreator, this.contentName);
+ this.transportId = this.mJingleConnectionManager.nextRandomId();
+ content.setTransportId(this.transportId);
+ content.ibbTransport().setAttribute("block-size",
+ Integer.toString(this.ibbBlockSize));
+ packet.setContent(content);
+ this.sendJinglePacket(packet);
+ }
+
+ private boolean receiveFallbackToIbb(JinglePacket packet) {
+ Logging.d(Config.LOGTAG, "receiving fallack to ibb");
+ String receivedBlockSize = packet.getJingleContent().ibbTransport()
+ .getAttribute("block-size");
+ if (receivedBlockSize != null) {
+ int bs = Integer.parseInt(receivedBlockSize);
+ if (bs > this.ibbBlockSize) {
+ this.ibbBlockSize = bs;
+ }
+ }
+ this.transportId = packet.getJingleContent().getTransportId();
+ this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
+ this.transport.receive(file, onFileTransmissionSatusChanged);
+ JinglePacket answer = bootstrapPacket("transport-accept");
+ Content content = new Content("initiator", "a-file-offer");
+ content.setTransportId(this.transportId);
+ content.ibbTransport().setAttribute("block-size",this.ibbBlockSize);
+ answer.setContent(content);
+ this.sendJinglePacket(answer);
+ return true;
+ }
+
+ private boolean receiveTransportAccept(JinglePacket packet) {
+ if (packet.getJingleContent().hasIbbTransport()) {
+ String receivedBlockSize = packet.getJingleContent().ibbTransport()
+ .getAttribute("block-size");
+ if (receivedBlockSize != null) {
+ int bs = Integer.parseInt(receivedBlockSize);
+ if (bs > this.ibbBlockSize) {
+ this.ibbBlockSize = bs;
+ }
+ }
+ this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
+ this.transport.connect(new OnTransportConnected() {
+
+ @Override
+ public void failed() {
+ Logging.d(Config.LOGTAG, "ibb open failed");
+ }
+
+ @Override
+ public void established() {
+ JingleConnection.this.transport.send(file,
+ onFileTransmissionSatusChanged);
+ }
+ });
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private void receiveSuccess() {
+ this.mJingleStatus = JINGLE_STATUS_FINISHED;
+ MessageUtil.markMessage(this.message,Message.STATUS_SEND_RECEIVED);
+ this.disconnectSocks5Connections();
+ if (this.transport != null && this.transport instanceof JingleInbandTransport) {
+ this.transport.disconnect();
+ }
+ this.message.setTransferable(null);
+ this.mJingleConnectionManager.finishConnection(this);
+ }
+
+ public void cancel() {
+ this.disconnectSocks5Connections();
+ if (this.transport != null && this.transport instanceof JingleInbandTransport) {
+ this.transport.disconnect();
+ }
+ this.sendCancel();
+ this.mJingleConnectionManager.finishConnection(this);
+ if (this.responder.equals(account.getJid())) {
+ this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_FAILED));
+ if (this.file!=null) {
+ file.delete();
+ }
+ this.mXmppConnectionService.updateConversationUi();
+ } else {
+ MessageUtil.markMessage(this.message, Message.STATUS_SEND_FAILED);
+ this.message.setTransferable(null);
+ }
+ }
+
+ private void fail() {
+ this.mJingleStatus = JINGLE_STATUS_FAILED;
+ this.disconnectSocks5Connections();
+ if (this.transport != null && this.transport instanceof JingleInbandTransport) {
+ this.transport.disconnect();
+ }
+ StreamUtil.close(mFileInputStream);
+ StreamUtil.close(mFileOutputStream);
+ if (this.message != null) {
+ if (this.responder.equals(account.getJid())) {
+ this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_FAILED));
+ if (this.file!=null) {
+ file.delete();
+ }
+ this.mXmppConnectionService.updateConversationUi();
+ } else {
+ MessageUtil.markMessage(this.message, Message.STATUS_SEND_FAILED);
+ this.message.setTransferable(null);
+ }
+ }
+ this.mJingleConnectionManager.finishConnection(this);
+ }
+
+ private void sendCancel() {
+ JinglePacket packet = bootstrapPacket("session-terminate");
+ Reason reason = new Reason();
+ reason.addChild("cancel");
+ packet.setReason(reason);
+ this.sendJinglePacket(packet);
+ }
+
+ private void connectNextCandidate() {
+ for (JingleCandidate candidate : this.candidates) {
+ if ((!connections.containsKey(candidate.getCid()) && (!candidate
+ .isOurs()))) {
+ this.connectWithCandidate(candidate);
+ return;
+ }
+ }
+ this.sendCandidateError();
+ }
+
+ private void connectWithCandidate(final JingleCandidate candidate) {
+ final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
+ this, candidate);
+ connections.put(candidate.getCid(), socksConnection);
+ socksConnection.connect(new OnTransportConnected() {
+
+ @Override
+ public void failed() {
+ Logging.d(Config.LOGTAG,
+ "connection failed with " + candidate.getHost() + ":"
+ + candidate.getPort());
+ connectNextCandidate();
+ }
+
+ @Override
+ public void established() {
+ Logging.d(Config.LOGTAG,
+ "established connection with " + candidate.getHost()
+ + ":" + candidate.getPort());
+ sendCandidateUsed(candidate.getCid());
+ }
+ });
+ }
+
+ private void disconnectSocks5Connections() {
+ Iterator<Entry<String, JingleSocks5Transport>> it = this.connections
+ .entrySet().iterator();
+ while (it.hasNext()) {
+ Entry<String, JingleSocks5Transport> pairs = it.next();
+ pairs.getValue().disconnect();
+ it.remove();
+ }
+ }
+
+ private void sendProxyActivated(String cid) {
+ JinglePacket packet = bootstrapPacket("transport-info");
+ Content content = new Content(this.contentCreator, this.contentName);
+ content.setTransportId(this.transportId);
+ content.socks5transport().addChild("activated")
+ .setAttribute("cid", cid);
+ packet.setContent(content);
+ this.sendJinglePacket(packet);
+ }
+
+ private void sendCandidateUsed(final String cid) {
+ JinglePacket packet = bootstrapPacket("transport-info");
+ Content content = new Content(this.contentCreator, this.contentName);
+ content.setTransportId(this.transportId);
+ content.socks5transport().addChild("candidate-used")
+ .setAttribute("cid", cid);
+ packet.setContent(content);
+ this.sentCandidate = true;
+ if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
+ connect();
+ }
+ this.sendJinglePacket(packet);
+ }
+
+ private void sendCandidateError() {
+ JinglePacket packet = bootstrapPacket("transport-info");
+ Content content = new Content(this.contentCreator, this.contentName);
+ content.setTransportId(this.transportId);
+ content.socks5transport().addChild("candidate-error");
+ packet.setContent(content);
+ this.sentCandidate = true;
+ if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
+ connect();
+ }
+ this.sendJinglePacket(packet);
+ }
+
+ public Jid getInitiator() {
+ return this.initiator;
+ }
+
+ public Jid getResponder() {
+ return this.responder;
+ }
+
+ public int getJingleStatus() {
+ return this.mJingleStatus;
+ }
+
+ private boolean equalCandidateExists(JingleCandidate candidate) {
+ for (JingleCandidate c : this.candidates) {
+ if (c.equalValues(candidate)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void mergeCandidate(JingleCandidate candidate) {
+ for (JingleCandidate c : this.candidates) {
+ if (c.equals(candidate)) {
+ return;
+ }
+ }
+ this.candidates.add(candidate);
+ }
+
+ private void mergeCandidates(List<JingleCandidate> candidates) {
+ for (JingleCandidate c : candidates) {
+ mergeCandidate(c);
+ }
+ }
+
+ private JingleCandidate getCandidate(String cid) {
+ for (JingleCandidate c : this.candidates) {
+ if (c.getCid().equals(cid)) {
+ return c;
+ }
+ }
+ return null;
+ }
+
+ public void updateProgress(int i) {
+ this.mProgress = i;
+ mXmppConnectionService.updateConversationUi();
+ }
+
+ interface OnProxyActivated {
+ public void success();
+
+ public void failed();
+ }
+
+ public boolean hasTransportId(String sid) {
+ return sid.equals(this.transportId);
+ }
+
+ public JingleTransport getTransport() {
+ return this.transport;
+ }
+
+ public boolean start() {
+ if (account.getStatus() == Account.State.ONLINE) {
+ if (mJingleStatus == JINGLE_STATUS_INITIATED) {
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ sendAccept();
+ }
+ }).start();
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public int getStatus() {
+ return this.mStatus;
+ }
+
+ @Override
+ public long getFileSize() {
+ if (this.file != null) {
+ return this.file.getExpectedSize();
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public int getProgress() {
+ return this.mProgress;
+ }
+
+ public AbstractConnectionManager getConnectionManager() {
+ return this.mJingleConnectionManager;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
new file mode 100644
index 00000000..19ac354d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
@@ -0,0 +1,170 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import android.annotation.SuppressLint;
+
+import java.math.BigInteger;
+import java.security.SecureRandom;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import de.thedevstack.android.logcat.Logging;
+
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.services.AbstractConnectionManager;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.Xmlns;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class JingleConnectionManager extends AbstractConnectionManager {
+ private List<JingleConnection> connections = new CopyOnWriteArrayList<>();
+
+ private HashMap<Jid, JingleCandidate> primaryCandidates = new HashMap<>();
+
+ @SuppressLint("TrulyRandom")
+ private SecureRandom random = new SecureRandom();
+
+ public void deliverPacket(Account account, JinglePacket packet) {
+ if (packet.isAction("session-initiate")) {
+ JingleConnection connection = new JingleConnection(this);
+ connection.init(account, packet);
+ connections.add(connection);
+ } else {
+ for (JingleConnection connection : connections) {
+ if (connection.getAccount() == account
+ && connection.getSessionId().equals(
+ packet.getSessionId())
+ && connection.getCounterPart().equals(packet.getFrom())) {
+ connection.deliverPacket(packet);
+ return;
+ }
+ }
+ IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR);
+ Element error = response.addChild("error");
+ error.setAttribute("type", "cancel");
+ error.addChild("item-not-found",
+ "urn:ietf:params:xml:ns:xmpp-stanzas");
+ error.addChild("unknown-session", "urn:xmpp:jingle:errors:1");
+ account.getXmppConnection().sendIqPacket(response, null);
+ }
+ }
+
+ public JingleConnection createNewConnection(Message message) {
+ Transferable old = message.getTransferable();
+ if (old != null) {
+ old.cancel();
+ }
+ JingleConnection connection = new JingleConnection(this);
+ MessageUtil.markMessage(message,Message.STATUS_WAITING);
+ connection.init(message);
+ this.connections.add(connection);
+ return connection;
+ }
+
+ public JingleConnection createNewConnection(final JinglePacket packet) {
+ JingleConnection connection = new JingleConnection(this);
+ this.connections.add(connection);
+ return connection;
+ }
+
+ public void finishConnection(JingleConnection connection) {
+ this.connections.remove(connection);
+ }
+
+ public void getPrimaryCandidate(Account account,
+ final OnPrimaryCandidateFound listener) {
+ if (Config.DISABLE_PROXY_LOOKUP) {
+ listener.onPrimaryCandidateFound(false, null);
+ return;
+ }
+ if (!this.primaryCandidates.containsKey(account.getJid().toBareJid())) {
+ final Jid proxy = account.getXmppConnection().findDiscoItemByFeature(Xmlns.BYTE_STREAMS);
+ if (proxy != null) {
+ IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
+ iq.setTo(proxy);
+ iq.query(Xmlns.BYTE_STREAMS);
+ account.getXmppConnection().sendIqPacket(iq,new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ Element streamhost = packet.query().findChild("streamhost",Xmlns.BYTE_STREAMS);
+ final String host = streamhost == null ? null : streamhost.getAttribute("host");
+ final String port = streamhost == null ? null : streamhost.getAttribute("port");
+ if (host != null && port != null) {
+ try {
+ JingleCandidate candidate = new JingleCandidate(nextRandomId(), true);
+ candidate.setHost(host);
+ candidate.setPort(Integer.parseInt(port));
+ candidate.setType(JingleCandidate.TYPE_PROXY);
+ candidate.setJid(proxy);
+ candidate.setPriority(655360 + 65535);
+ primaryCandidates.put(account.getJid().toBareJid(),candidate);
+ listener.onPrimaryCandidateFound(true,candidate);
+ } catch (final NumberFormatException e) {
+ listener.onPrimaryCandidateFound(false,null);
+ return;
+ }
+ } else {
+ listener.onPrimaryCandidateFound(false,null);
+ }
+ }
+ });
+ } else {
+ listener.onPrimaryCandidateFound(false, null);
+ }
+
+ } else {
+ listener.onPrimaryCandidateFound(true,
+ this.primaryCandidates.get(account.getJid().toBareJid()));
+ }
+ }
+
+ public String nextRandomId() {
+ return new BigInteger(50, random).toString(32);
+ }
+
+ public void deliverIbbPacket(Account account, IqPacket packet) {
+ String sid = null;
+ Element payload = null;
+ if (packet.hasChild("open", "http://jabber.org/protocol/ibb")) {
+ payload = packet.findChild("open", "http://jabber.org/protocol/ibb");
+ sid = payload.getAttribute("sid");
+ } else if (packet.hasChild("data", "http://jabber.org/protocol/ibb")) {
+ payload = packet.findChild("data", "http://jabber.org/protocol/ibb");
+ sid = payload.getAttribute("sid");
+ }
+ if (sid != null) {
+ for (JingleConnection connection : connections) {
+ if (connection.getAccount() == account
+ && connection.hasTransportId(sid)) {
+ JingleTransport transport = connection.getTransport();
+ if (transport instanceof JingleInbandTransport) {
+ JingleInbandTransport inbandTransport = (JingleInbandTransport) transport;
+ inbandTransport.deliverPayload(packet, payload);
+ return;
+ }
+ }
+ }
+ Logging.d(Config.LOGTAG,"couldn't deliver payload: " + payload.toString());
+ } else {
+ Logging.d(Config.LOGTAG, "no sid found in incoming ibb packet");
+ }
+ }
+
+ public void cancelInTransmission() {
+ for (JingleConnection connection : this.connections) {
+ if (connection.getJingleStatus() == JingleConnection.JINGLE_STATUS_TRANSMITTING) {
+ connection.cancel();
+ }
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java
new file mode 100644
index 00000000..3800b94f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java
@@ -0,0 +1,239 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import android.util.Base64;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.utils.StreamUtil;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class JingleInbandTransport extends JingleTransport {
+
+ private Account account;
+ private Jid counterpart;
+ private int blockSize;
+ private int seq = 0;
+ private String sessionId;
+
+ private boolean established = false;
+
+ private boolean connected = true;
+
+ private DownloadableFile file;
+ private JingleConnection connection;
+
+ private InputStream fileInputStream = null;
+ private OutputStream fileOutputStream = null;
+ private long remainingSize = 0;
+ private long fileSize = 0;
+ private MessageDigest digest;
+
+ private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged;
+
+ private OnIqPacketReceived onAckReceived = new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (connected && packet.getType() == IqPacket.TYPE.RESULT) {
+ sendNextBlock();
+ }
+ }
+ };
+
+ public JingleInbandTransport(final JingleConnection connection, final String sid, final int blocksize) {
+ this.connection = connection;
+ this.account = connection.getAccount();
+ this.counterpart = connection.getCounterPart();
+ this.blockSize = blocksize;
+ this.sessionId = sid;
+ }
+
+ public void connect(final OnTransportConnected callback) {
+ IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
+ iq.setTo(this.counterpart);
+ Element open = iq.addChild("open", "http://jabber.org/protocol/ibb");
+ open.setAttribute("sid", this.sessionId);
+ open.setAttribute("stanza", "iq");
+ open.setAttribute("block-size", Integer.toString(this.blockSize));
+ this.connected = true;
+ this.account.getXmppConnection().sendIqPacket(iq,
+ new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account,
+ IqPacket packet) {
+ if (packet.getType() != IqPacket.TYPE.RESULT) {
+ callback.failed();
+ } else {
+ callback.established();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void receive(DownloadableFile file,
+ OnFileTransmissionStatusChanged callback) {
+ this.onFileTransmissionStatusChanged = callback;
+ this.file = file;
+ try {
+ this.digest = MessageDigest.getInstance("SHA-1");
+ digest.reset();
+ file.getParentFile().mkdirs();
+ file.createNewFile();
+ this.fileOutputStream = connection.getFileOutputStream();
+ if (this.fileOutputStream == null) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": could not create output stream");
+ callback.onFileTransferAborted();
+ return;
+ }
+ this.remainingSize = this.fileSize = file.getExpectedSize();
+ } catch (final NoSuchAlgorithmException | IOException e) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+" "+e.getMessage());
+ callback.onFileTransferAborted();
+ }
+ }
+
+ @Override
+ public void send(DownloadableFile file,
+ OnFileTransmissionStatusChanged callback) {
+ this.onFileTransmissionStatusChanged = callback;
+ this.file = file;
+ try {
+ this.remainingSize = this.file.getExpectedSize();
+ this.fileSize = this.remainingSize;
+ this.digest = MessageDigest.getInstance("SHA-1");
+ this.digest.reset();
+ fileInputStream = connection.getFileInputStream();
+ if (fileInputStream == null) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": could no create input stream");
+ callback.onFileTransferAborted();
+ return;
+ }
+ if (this.connected) {
+ this.sendNextBlock();
+ }
+ } catch (NoSuchAlgorithmException e) {
+ callback.onFileTransferAborted();
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": "+e.getMessage());
+ }
+ }
+
+ @Override
+ public void disconnect() {
+ this.connected = false;
+ if (this.fileOutputStream != null) {
+ try {
+ this.fileOutputStream.close();
+ } catch (IOException e) {
+
+ }
+ }
+ if (this.fileInputStream != null) {
+ try {
+ this.fileInputStream.close();
+ } catch (IOException e) {
+
+ }
+ }
+ }
+
+ private void sendNextBlock() {
+ byte[] buffer = new byte[this.blockSize];
+ try {
+ int count = fileInputStream.read(buffer);
+ if (count == -1) {
+ file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
+ this.onFileTransmissionStatusChanged.onFileTransmitted(file);
+ fileInputStream.close();
+ return;
+ } else if (count != buffer.length) {
+ int rem = fileInputStream.read(buffer,count,buffer.length-count);
+ if (rem > 0) {
+ count += rem;
+ }
+ }
+ this.remainingSize -= count;
+ this.digest.update(buffer,0,count);
+ String base64 = Base64.encodeToString(buffer,0,count, Base64.NO_WRAP);
+ IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
+ iq.setTo(this.counterpart);
+ Element data = iq.addChild("data", "http://jabber.org/protocol/ibb");
+ data.setAttribute("seq", Integer.toString(this.seq));
+ data.setAttribute("block-size", Integer.toString(this.blockSize));
+ data.setAttribute("sid", this.sessionId);
+ data.setContent(base64);
+ this.account.getXmppConnection().sendIqPacket(iq, this.onAckReceived);
+ this.account.getXmppConnection().r(); //don't fill up stanza queue too much
+ this.seq++;
+ if (this.remainingSize > 0) {
+ connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
+ } else {
+ file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
+ this.onFileTransmissionStatusChanged.onFileTransmitted(file);
+ fileInputStream.close();
+ }
+ } catch (IOException e) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": "+e.getMessage());
+ StreamUtil.close(fileInputStream);
+ this.onFileTransmissionStatusChanged.onFileTransferAborted();
+ }
+ }
+
+ private void receiveNextBlock(String data) {
+ try {
+ byte[] buffer = Base64.decode(data, Base64.NO_WRAP);
+ if (this.remainingSize < buffer.length) {
+ buffer = Arrays.copyOfRange(buffer, 0, (int) this.remainingSize);
+ }
+ this.remainingSize -= buffer.length;
+ this.fileOutputStream.write(buffer);
+ this.digest.update(buffer);
+ if (this.remainingSize <= 0) {
+ file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
+ fileOutputStream.flush();
+ fileOutputStream.close();
+ this.onFileTransmissionStatusChanged.onFileTransmitted(file);
+ } else {
+ connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
+ }
+ } catch (IOException e) {
+ Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": "+e.getMessage());
+ StreamUtil.close(fileOutputStream);
+ this.onFileTransmissionStatusChanged.onFileTransferAborted();
+ }
+ }
+
+ public void deliverPayload(IqPacket packet, Element payload) {
+ if (payload.getName().equals("open")) {
+ if (!established) {
+ established = true;
+ connected = true;
+ this.receiveNextBlock("");
+ this.account.getXmppConnection().sendIqPacket(
+ packet.generateResponse(IqPacket.TYPE.RESULT), null);
+ } else {
+ this.account.getXmppConnection().sendIqPacket(
+ packet.generateResponse(IqPacket.TYPE.ERROR), null);
+ }
+ } else if (connected && payload.getName().equals("data")) {
+ this.receiveNextBlock(payload.getContent());
+ this.account.getXmppConnection().sendIqPacket(
+ packet.generateResponse(IqPacket.TYPE.RESULT), null);
+ } else {
+ // TODO some sort of exception
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java
new file mode 100644
index 00000000..318a35f7
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java
@@ -0,0 +1,217 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import android.os.PowerManager;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import de.thedevstack.conversationsplus.utils.StreamUtil;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.SocksSocketFactory;
+
+public class JingleSocks5Transport extends JingleTransport {
+ private JingleCandidate candidate;
+ private JingleConnection connection;
+ private String destination;
+ private OutputStream outputStream;
+ private InputStream inputStream;
+ private boolean isEstablished = false;
+ private boolean activated = false;
+ protected Socket socket;
+
+ public JingleSocks5Transport(JingleConnection jingleConnection,
+ JingleCandidate candidate) {
+ this.candidate = candidate;
+ this.connection = jingleConnection;
+ try {
+ MessageDigest mDigest = MessageDigest.getInstance("SHA-1");
+ StringBuilder destBuilder = new StringBuilder();
+ destBuilder.append(jingleConnection.getSessionId());
+ if (candidate.isOurs()) {
+ destBuilder.append(jingleConnection.getAccount().getJid());
+ destBuilder.append(jingleConnection.getCounterPart());
+ } else {
+ destBuilder.append(jingleConnection.getCounterPart());
+ destBuilder.append(jingleConnection.getAccount().getJid());
+ }
+ mDigest.reset();
+ this.destination = CryptoHelper.bytesToHex(mDigest
+ .digest(destBuilder.toString().getBytes()));
+ } catch (NoSuchAlgorithmException e) {
+
+ }
+ }
+
+ public void connect(final OnTransportConnected callback) {
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ try {
+ socket = new Socket();
+ SocketAddress address = new InetSocketAddress(candidate.getHost(),candidate.getPort());
+ socket.connect(address,Config.SOCKET_TIMEOUT * 1000);
+
+ inputStream = socket.getInputStream();
+ outputStream = socket.getOutputStream();
+ SocksSocketFactory.createSocksConnection(socket,destination,0);
+ isEstablished = true;
+ callback.established();
+ } catch (UnknownHostException e) {
+ callback.failed();
+ } catch (IOException e) {
+ callback.failed();
+ }
+ }
+ }).start();
+
+ }
+
+ public void send(final DownloadableFile file, final OnFileTransmissionStatusChanged callback) {
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ InputStream fileInputStream = null;
+ final PowerManager.WakeLock wakeLock = ConversationsPlusApplication.createPartialWakeLock("jingle_send_"+connection.getSessionId());
+ try {
+ wakeLock.acquire();
+ MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ digest.reset();
+ fileInputStream = connection.getFileInputStream();
+ if (fileInputStream == null) {
+ Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": could not create input stream");
+ callback.onFileTransferAborted();
+ return;
+ }
+ long size = file.getExpectedSize();
+ long transmitted = 0;
+ int count;
+ byte[] buffer = new byte[8192];
+ while ((count = fileInputStream.read(buffer)) > 0) {
+ outputStream.write(buffer, 0, count);
+ digest.update(buffer, 0, count);
+ transmitted += count;
+ connection.updateProgress((int) ((((double) transmitted) / size) * 100));
+ }
+ outputStream.flush();
+ file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
+ if (callback != null) {
+ callback.onFileTransmitted(file);
+ }
+ } catch (FileNotFoundException e) {
+ Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage());
+ callback.onFileTransferAborted();
+ } catch (IOException e) {
+ Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage());
+ callback.onFileTransferAborted();
+ } catch (NoSuchAlgorithmException e) {
+ Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage());
+ callback.onFileTransferAborted();
+ } finally {
+ StreamUtil.close(fileInputStream);
+ wakeLock.release();
+ }
+ }
+ }).start();
+
+ }
+
+ public void receive(final DownloadableFile file, final OnFileTransmissionStatusChanged callback) {
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ OutputStream fileOutputStream = null;
+ final PowerManager.WakeLock wakeLock = ConversationsPlusApplication.createPartialWakeLock("jingle_receive_"+connection.getSessionId());
+ try {
+ wakeLock.acquire();
+ MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ digest.reset();
+ //inputStream.skip(45);
+ socket.setSoTimeout(30000);
+ file.getParentFile().mkdirs();
+ file.createNewFile();
+ fileOutputStream = connection.getFileOutputStream();
+ if (fileOutputStream == null) {
+ callback.onFileTransferAborted();
+ Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": could not create output stream");
+ return;
+ }
+ double size = file.getExpectedSize();
+ long remainingSize = file.getExpectedSize();
+ byte[] buffer = new byte[8192];
+ int count;
+ while (remainingSize > 0) {
+ count = inputStream.read(buffer);
+ if (count == -1) {
+ callback.onFileTransferAborted();
+ Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": file ended prematurely with "+remainingSize+" bytes remaining");
+ return;
+ } else {
+ fileOutputStream.write(buffer, 0, count);
+ digest.update(buffer, 0, count);
+ remainingSize -= count;
+ }
+ connection.updateProgress((int) (((size - remainingSize) / size) * 100));
+ }
+ fileOutputStream.flush();
+ fileOutputStream.close();
+ file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
+ callback.onFileTransmitted(file);
+ } catch (FileNotFoundException e) {
+ Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage());
+ callback.onFileTransferAborted();
+ } catch (IOException e) {
+ Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage());
+ callback.onFileTransferAborted();
+ } catch (NoSuchAlgorithmException e) {
+ Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage());
+ callback.onFileTransferAborted();
+ } finally {
+ wakeLock.release();
+ StreamUtil.close(fileOutputStream);
+ StreamUtil.close(inputStream);
+ }
+ }
+ }).start();
+ }
+
+ public boolean isProxy() {
+ return this.candidate.getType() == JingleCandidate.TYPE_PROXY;
+ }
+
+ public boolean needsActivation() {
+ return (this.isProxy() && !this.activated);
+ }
+
+ public void disconnect() {
+ StreamUtil.close(inputStream);
+ StreamUtil.close(outputStream);
+ StreamUtil.close(socket);
+ }
+
+ public boolean isEstablished() {
+ return this.isEstablished;
+ }
+
+ public JingleCandidate getCandidate() {
+ return this.candidate;
+ }
+
+ public void setActivated(boolean activated) {
+ this.activated = activated;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java
new file mode 100644
index 00000000..e832d3f5
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java
@@ -0,0 +1,15 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import eu.siacs.conversations.entities.DownloadableFile;
+
+public abstract class JingleTransport {
+ public abstract void connect(final OnTransportConnected callback);
+
+ public abstract void receive(final DownloadableFile file,
+ final OnFileTransmissionStatusChanged callback);
+
+ public abstract void send(final DownloadableFile file,
+ final OnFileTransmissionStatusChanged callback);
+
+ public abstract void disconnect();
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java
new file mode 100644
index 00000000..91cba39f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java
@@ -0,0 +1,9 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import eu.siacs.conversations.entities.DownloadableFile;
+
+public interface OnFileTransmissionStatusChanged {
+ void onFileTransmitted(DownloadableFile file);
+
+ void onFileTransferAborted();
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java
new file mode 100644
index 00000000..9a60b392
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java
@@ -0,0 +1,9 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xmpp.PacketReceived;
+import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+
+public interface OnJinglePacketReceived extends PacketReceived {
+ void onJinglePacketReceived(Account account, JinglePacket packet);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java
new file mode 100644
index 00000000..76e33717
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java
@@ -0,0 +1,5 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+public interface OnPrimaryCandidateFound {
+ void onPrimaryCandidateFound(boolean success, JingleCandidate canditate);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java
new file mode 100644
index 00000000..38f03c5d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+public interface OnTransportConnected {
+ public void failed();
+
+ public void established();
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java
new file mode 100644
index 00000000..f752cc5d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java
@@ -0,0 +1,103 @@
+package eu.siacs.conversations.xmpp.jingle.stanzas;
+
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.xml.Element;
+
+public class Content extends Element {
+
+ private String transportId;
+
+ private Content(String name) {
+ super(name);
+ }
+
+ public Content() {
+ super("content");
+ }
+
+ public Content(String creator, String name) {
+ super("content");
+ this.setAttribute("creator", creator);
+ this.setAttribute("name", name);
+ }
+
+ public void setTransportId(String sid) {
+ this.transportId = sid;
+ }
+
+ public Element setFileOffer(DownloadableFile actualFile, boolean otr) {
+ Element description = this.addChild("description",
+ "urn:xmpp:jingle:apps:file-transfer:3");
+ Element offer = description.addChild("offer");
+ Element file = offer.addChild("file");
+ file.addChild("size").setContent(Long.toString(actualFile.getExpectedSize()));
+ if (otr) {
+ file.addChild("name").setContent(actualFile.getName() + ".otr");
+ } else {
+ file.addChild("name").setContent(actualFile.getName());
+ }
+ return file;
+ }
+
+ public Element getFileOffer() {
+ Element description = this.findChild("description",
+ "urn:xmpp:jingle:apps:file-transfer:3");
+ if (description == null) {
+ return null;
+ }
+ Element offer = description.findChild("offer");
+ if (offer == null) {
+ return null;
+ }
+ return offer.findChild("file");
+ }
+
+ public void setFileOffer(Element fileOffer) {
+ Element description = this.findChild("description",
+ "urn:xmpp:jingle:apps:file-transfer:3");
+ if (description == null) {
+ description = this.addChild("description",
+ "urn:xmpp:jingle:apps:file-transfer:3");
+ }
+ description.addChild(fileOffer);
+ }
+
+ public String getTransportId() {
+ if (hasSocks5Transport()) {
+ this.transportId = socks5transport().getAttribute("sid");
+ } else if (hasIbbTransport()) {
+ this.transportId = ibbTransport().getAttribute("sid");
+ }
+ return this.transportId;
+ }
+
+ public Element socks5transport() {
+ Element transport = this.findChild("transport",
+ "urn:xmpp:jingle:transports:s5b:1");
+ if (transport == null) {
+ transport = this.addChild("transport",
+ "urn:xmpp:jingle:transports:s5b:1");
+ transport.setAttribute("sid", this.transportId);
+ }
+ return transport;
+ }
+
+ public Element ibbTransport() {
+ Element transport = this.findChild("transport",
+ "urn:xmpp:jingle:transports:ibb:1");
+ if (transport == null) {
+ transport = this.addChild("transport",
+ "urn:xmpp:jingle:transports:ibb:1");
+ transport.setAttribute("sid", this.transportId);
+ }
+ return transport;
+ }
+
+ public boolean hasSocks5Transport() {
+ return this.hasChild("transport", "urn:xmpp:jingle:transports:s5b:1");
+ }
+
+ public boolean hasIbbTransport() {
+ return this.hasChild("transport", "urn:xmpp:jingle:transports:ibb:1");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java
new file mode 100644
index 00000000..4f73a83a
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java
@@ -0,0 +1,96 @@
+package eu.siacs.conversations.xmpp.jingle.stanzas;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class JinglePacket extends IqPacket {
+ Content content = null;
+ Reason reason = null;
+ Element jingle = new Element("jingle");
+
+ @Override
+ public Element addChild(Element child) {
+ if ("jingle".equals(child.getName())) {
+ Element contentElement = child.findChild("content");
+ if (contentElement != null) {
+ this.content = new Content();
+ this.content.setChildren(contentElement.getChildren());
+ this.content.setAttributes(contentElement.getAttributes());
+ }
+ Element reasonElement = child.findChild("reason");
+ if (reasonElement != null) {
+ this.reason = new Reason();
+ this.reason.setChildren(reasonElement.getChildren());
+ this.reason.setAttributes(reasonElement.getAttributes());
+ }
+ this.jingle.setAttributes(child.getAttributes());
+ }
+ return child;
+ }
+
+ public JinglePacket setContent(Content content) {
+ this.content = content;
+ return this;
+ }
+
+ public Content getJingleContent() {
+ if (this.content == null) {
+ this.content = new Content();
+ }
+ return this.content;
+ }
+
+ public JinglePacket setReason(Reason reason) {
+ this.reason = reason;
+ return this;
+ }
+
+ public Reason getReason() {
+ return this.reason;
+ }
+
+ private void build() {
+ this.children.clear();
+ this.jingle.clearChildren();
+ this.jingle.setAttribute("xmlns", "urn:xmpp:jingle:1");
+ if (this.content != null) {
+ jingle.addChild(this.content);
+ }
+ if (this.reason != null) {
+ jingle.addChild(this.reason);
+ }
+ this.children.add(jingle);
+ this.setAttribute("type", "set");
+ }
+
+ public String getSessionId() {
+ return this.jingle.getAttribute("sid");
+ }
+
+ public void setSessionId(String sid) {
+ this.jingle.setAttribute("sid", sid);
+ }
+
+ @Override
+ public String toString() {
+ this.build();
+ return super.toString();
+ }
+
+ public void setAction(String action) {
+ this.jingle.setAttribute("action", action);
+ }
+
+ public String getAction() {
+ return this.jingle.getAttribute("action");
+ }
+
+ public void setInitiator(final Jid initiator) {
+ this.jingle.setAttribute("initiator", initiator.toString());
+ }
+
+ public boolean isAction(String action) {
+ return action.equalsIgnoreCase(this.getAction());
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java
new file mode 100644
index 00000000..610d5e76
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java
@@ -0,0 +1,13 @@
+package eu.siacs.conversations.xmpp.jingle.stanzas;
+
+import eu.siacs.conversations.xml.Element;
+
+public class Reason extends Element {
+ private Reason(String name) {
+ super(name);
+ }
+
+ public Reason() {
+ super("reason");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java
new file mode 100644
index 00000000..38bb5c8f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java
@@ -0,0 +1,102 @@
+package eu.siacs.conversations.xmpp.pep;
+
+import android.util.Base64;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class Avatar {
+
+ public enum Origin { PEP, VCARD };
+
+ public String type;
+ public String sha1sum;
+ public String image;
+ public int height;
+ public int width;
+ public long size;
+ public Jid owner;
+ public Origin origin = Origin.PEP; //default to maintain compat
+
+ public byte[] getImageAsBytes() {
+ return Base64.decode(image, Base64.DEFAULT);
+ }
+
+ public String getFilename() {
+ return sha1sum;
+ }
+
+ public static Avatar parseMetadata(Element items) {
+ Element item = items.findChild("item");
+ if (item == null) {
+ return null;
+ }
+ Element metadata = item.findChild("metadata");
+ if (metadata == null) {
+ return null;
+ }
+ String primaryId = item.getAttribute("id");
+ if (primaryId == null) {
+ return null;
+ }
+ for (Element child : metadata.getChildren()) {
+ if (child.getName().equals("info")
+ && primaryId.equals(child.getAttribute("id"))) {
+ Avatar avatar = new Avatar();
+ String height = child.getAttribute("height");
+ String width = child.getAttribute("width");
+ String size = child.getAttribute("bytes");
+ try {
+ if (height != null) {
+ avatar.height = Integer.parseInt(height);
+ }
+ if (width != null) {
+ avatar.width = Integer.parseInt(width);
+ }
+ if (size != null) {
+ avatar.size = Long.parseLong(size);
+ }
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ avatar.type = child.getAttribute("type");
+ String hash = child.getAttribute("id");
+ if (!isValidSHA1(hash)) {
+ return null;
+ }
+ avatar.sha1sum = hash;
+ avatar.origin = Origin.PEP;
+ return avatar;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (object != null && object instanceof Avatar) {
+ Avatar other = (Avatar) object;
+ return other.getFilename().equals(this.getFilename());
+ } else {
+ return false;
+ }
+ }
+
+ public static Avatar parsePresence(Element x) {
+ String hash = x == null ? null : x.findChildContent("photo");
+ if (hash == null) {
+ return null;
+ }
+ if (!isValidSHA1(hash)) {
+ return null;
+ }
+ Avatar avatar = new Avatar();
+ avatar.sha1sum = hash;
+ avatar.origin = Origin.VCARD;
+ return avatar;
+ }
+
+ private static boolean isValidSHA1(String s) {
+ return s != null && s.matches("[a-fA-F0-9]{40}");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java
new file mode 100644
index 00000000..fa5e6fbd
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java
@@ -0,0 +1,31 @@
+package eu.siacs.conversations.xmpp.stanzas;
+
+import eu.siacs.conversations.xml.Element;
+
+abstract public class AbstractAcknowledgeableStanza extends AbstractStanza {
+
+ protected AbstractAcknowledgeableStanza(String name) {
+ super(name);
+ }
+
+
+ public String getId() {
+ return this.getAttribute("id");
+ }
+
+ public void setId(final String id) {
+ setAttribute("id", id);
+ }
+
+ public Element getError() {
+ Element error = findChild("error");
+ if (error != null) {
+ for(Element element : error.getChildren()) {
+ if (!element.getName().equals("text")) {
+ return element;
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java
new file mode 100644
index 00000000..a6144df2
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java
@@ -0,0 +1,50 @@
+package eu.siacs.conversations.xmpp.stanzas;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class AbstractStanza extends Element {
+
+ protected AbstractStanza(final String name) {
+ super(name);
+ }
+
+ public Jid getTo() {
+ return getAttributeAsJid("to");
+ }
+
+ public Jid getFrom() {
+ return getAttributeAsJid("from");
+ }
+
+ public void setTo(final Jid to) {
+ if (to != null) {
+ setAttribute("to", to.toString());
+ }
+ }
+
+ public void setFrom(final Jid from) {
+ if (from != null) {
+ setAttribute("from", from.toString());
+ }
+ }
+
+ public boolean fromServer(final Account account) {
+ return getFrom() == null
+ || getFrom().equals(account.getServer())
+ || getFrom().equals(account.getJid().toBareJid())
+ || getFrom().equals(account.getJid());
+ }
+
+ public boolean toServer(final Account account) {
+ return getTo() == null
+ || getTo().equals(account.getServer())
+ || getTo().equals(account.getJid().toBareJid())
+ || getTo().equals(account.getJid());
+ }
+
+ public boolean fromAccount(final Account account) {
+ return getFrom() != null && getFrom().toBareJid().equals(account.getJid().toBareJid());
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java
new file mode 100644
index 00000000..302dc78e
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java
@@ -0,0 +1,69 @@
+package eu.siacs.conversations.xmpp.stanzas;
+
+import eu.siacs.conversations.xml.Element;
+
+public class IqPacket extends AbstractAcknowledgeableStanza {
+
+ public enum TYPE {
+ ERROR,
+ SET,
+ RESULT,
+ GET,
+ INVALID,
+ TIMEOUT
+ }
+
+ public IqPacket(final TYPE type) {
+ super("iq");
+ if (type != TYPE.INVALID) {
+ this.setAttribute("type", type.toString().toLowerCase());
+ }
+ }
+
+ public IqPacket() {
+ super("iq");
+ }
+
+ public Element query() {
+ Element query = findChild("query");
+ if (query == null) {
+ query = addChild("query");
+ }
+ return query;
+ }
+
+ public Element query(final String xmlns) {
+ final Element query = query();
+ query.setAttribute("xmlns", xmlns);
+ return query();
+ }
+
+ public TYPE getType() {
+ final String type = getAttribute("type");
+ if (type == null) {
+ return TYPE.INVALID;
+ }
+ switch (type) {
+ case "error":
+ return TYPE.ERROR;
+ case "result":
+ return TYPE.RESULT;
+ case "set":
+ return TYPE.SET;
+ case "get":
+ return TYPE.GET;
+ case "timeout":
+ return TYPE.TIMEOUT;
+ default:
+ return TYPE.INVALID;
+ }
+ }
+
+ public IqPacket generateResponse(final TYPE type) {
+ final IqPacket packet = new IqPacket(type);
+ packet.setTo(this.getFrom());
+ packet.setId(this.getId());
+ return packet;
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java
new file mode 100644
index 00000000..941b4b5f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java
@@ -0,0 +1,99 @@
+package eu.siacs.conversations.xmpp.stanzas;
+
+import android.util.Pair;
+
+import eu.siacs.conversations.parser.AbstractParser;
+import eu.siacs.conversations.xml.Element;
+
+public class MessagePacket extends AbstractAcknowledgeableStanza {
+ public static final int TYPE_CHAT = 0;
+ public static final int TYPE_NORMAL = 2;
+ public static final int TYPE_GROUPCHAT = 3;
+ public static final int TYPE_ERROR = 4;
+ public static final int TYPE_HEADLINE = 5;
+
+ public MessagePacket() {
+ super("message");
+ }
+
+ public String getBody() {
+ return findChildContent("body");
+ }
+
+ public void setBody(String text) {
+ this.children.remove(findChild("body"));
+ Element body = new Element("body");
+ body.setContent(text);
+ this.children.add(0, body);
+ }
+
+ public void setAxolotlMessage(Element axolotlMessage) {
+ this.children.remove(findChild("body"));
+ this.children.add(0, axolotlMessage);
+ }
+
+ public void setType(int type) {
+ switch (type) {
+ case TYPE_CHAT:
+ this.setAttribute("type", "chat");
+ break;
+ case TYPE_GROUPCHAT:
+ this.setAttribute("type", "groupchat");
+ break;
+ case TYPE_NORMAL:
+ break;
+ case TYPE_ERROR:
+ this.setAttribute("type","error");
+ break;
+ default:
+ this.setAttribute("type", "chat");
+ break;
+ }
+ }
+
+ public int getType() {
+ String type = getAttribute("type");
+ if (type == null) {
+ return TYPE_NORMAL;
+ } else if (type.equals("normal")) {
+ return TYPE_NORMAL;
+ } else if (type.equals("chat")) {
+ return TYPE_CHAT;
+ } else if (type.equals("groupchat")) {
+ return TYPE_GROUPCHAT;
+ } else if (type.equals("error")) {
+ return TYPE_ERROR;
+ } else if (type.equals("headline")) {
+ return TYPE_HEADLINE;
+ } else {
+ return TYPE_NORMAL;
+ }
+ }
+
+ public Pair<MessagePacket,Long> getForwardedMessagePacket(String name, String namespace) {
+ Element wrapper = findChild(name, namespace);
+ if (wrapper == null) {
+ return null;
+ }
+ Element forwarded = wrapper.findChild("forwarded", "urn:xmpp:forward:0");
+ if (forwarded == null) {
+ return null;
+ }
+ MessagePacket packet = create(forwarded.findChild("message"));
+ if (packet == null) {
+ return null;
+ }
+ Long timestamp = AbstractParser.getTimestamp(forwarded,null);
+ return new Pair(packet,timestamp);
+ }
+
+ public static MessagePacket create(Element element) {
+ if (element == null) {
+ return null;
+ }
+ MessagePacket packet = new MessagePacket();
+ packet.setAttributes(element.getAttributes());
+ packet.setChildren(element.getChildren());
+ return packet;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java
new file mode 100644
index 00000000..c321498d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java
@@ -0,0 +1,8 @@
+package eu.siacs.conversations.xmpp.stanzas;
+
+public class PresencePacket extends AbstractAcknowledgeableStanza {
+
+ public PresencePacket() {
+ super("presence");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java
new file mode 100644
index 00000000..78ab66d8
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java
@@ -0,0 +1,10 @@
+package eu.siacs.conversations.xmpp.stanzas.csi;
+
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+
+public class ActivePacket extends AbstractStanza {
+ public ActivePacket() {
+ super("active");
+ setAttribute("xmlns", "urn:xmpp:csi:0");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java
new file mode 100644
index 00000000..f109280f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java
@@ -0,0 +1,10 @@
+package eu.siacs.conversations.xmpp.stanzas.csi;
+
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+
+public class InactivePacket extends AbstractStanza {
+ public InactivePacket() {
+ super("inactive");
+ setAttribute("xmlns", "urn:xmpp:csi:0");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java
new file mode 100644
index 00000000..f93b5d87
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java
@@ -0,0 +1,13 @@
+package eu.siacs.conversations.xmpp.stanzas.streammgmt;
+
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+
+public class AckPacket extends AbstractStanza {
+
+ public AckPacket(int sequence, int smVersion) {
+ super("a");
+ this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion);
+ this.setAttribute("h", Integer.toString(sequence));
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java
new file mode 100644
index 00000000..78cd81ed
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java
@@ -0,0 +1,13 @@
+package eu.siacs.conversations.xmpp.stanzas.streammgmt;
+
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+
+public class EnablePacket extends AbstractStanza {
+
+ public EnablePacket(int smVersion) {
+ super("enable");
+ this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion);
+ this.setAttribute("resume", "true");
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java
new file mode 100644
index 00000000..98cfc748
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java
@@ -0,0 +1,12 @@
+package eu.siacs.conversations.xmpp.stanzas.streammgmt;
+
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+
+public class RequestPacket extends AbstractStanza {
+
+ public RequestPacket(int smVersion) {
+ super("r");
+ this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion);
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java
new file mode 100644
index 00000000..9cdcfa5e
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java
@@ -0,0 +1,14 @@
+package eu.siacs.conversations.xmpp.stanzas.streammgmt;
+
+import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+
+public class ResumePacket extends AbstractStanza {
+
+ public ResumePacket(String id, int sequence, int smVersion) {
+ super("resume");
+ this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion);
+ this.setAttribute("previd", id);
+ this.setAttribute("h", Integer.toString(sequence));
+ }
+
+}