diff options
author | steckbrief <steckbrief@chefmail.de> | 2015-05-03 22:25:46 +0200 |
---|---|---|
committer | lookshe <github@lookshe.org> | 2015-06-19 09:46:40 +0200 |
commit | 7382e3af9769f76fe4e19934a59e45a3f9858332 (patch) | |
tree | c37cdb03dfaeaccde7c8dd7c79887bf0de278f83 /src/main/java/de/thedevstack/conversationsplus/xmpp | |
parent | b3b4a2902e37fb072e800f5dff0392755f5d4501 (diff) |
renaming eu.siacs.conversations to de.thedevstack.conversationsplus
"renaming eu.siacs.conversations to de.thedevstack.conversationsplus"
package renaming completed
Diffstat (limited to 'src/main/java/de/thedevstack/conversationsplus/xmpp')
40 files changed, 3973 insertions, 0 deletions
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/OnAdvancedStreamFeaturesLoaded.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnAdvancedStreamFeaturesLoaded.java new file mode 100644 index 00000000..0692232c --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnAdvancedStreamFeaturesLoaded.java @@ -0,0 +1,7 @@ +package de.thedevstack.conversationsplus.xmpp; + +import de.thedevstack.conversationsplus.entities.Account; + +public interface OnAdvancedStreamFeaturesLoaded { + public void onAdvancedStreamFeaturesAvailable(final Account account); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/OnBindListener.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnBindListener.java new file mode 100644 index 00000000..274511e0 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnBindListener.java @@ -0,0 +1,7 @@ +package de.thedevstack.conversationsplus.xmpp; + +import de.thedevstack.conversationsplus.entities.Account; + +public interface OnBindListener { + public void onBind(Account account); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/OnContactStatusChanged.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnContactStatusChanged.java new file mode 100644 index 00000000..009b33df --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnContactStatusChanged.java @@ -0,0 +1,7 @@ +package de.thedevstack.conversationsplus.xmpp; + +import de.thedevstack.conversationsplus.entities.Contact; + +public interface OnContactStatusChanged { + public void onContactStatusChanged(final Contact contact, final boolean online); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/OnIqPacketReceived.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnIqPacketReceived.java new file mode 100644 index 00000000..df0178c6 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnIqPacketReceived.java @@ -0,0 +1,8 @@ +package de.thedevstack.conversationsplus.xmpp; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; + +public interface OnIqPacketReceived extends PacketReceived { + public void onIqPacketReceived(Account account, IqPacket packet); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/OnMessageAcknowledged.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnMessageAcknowledged.java new file mode 100644 index 00000000..7cfa557d --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnMessageAcknowledged.java @@ -0,0 +1,7 @@ +package de.thedevstack.conversationsplus.xmpp; + +import de.thedevstack.conversationsplus.entities.Account; + +public interface OnMessageAcknowledged { + public void onMessageAcknowledged(Account account, String id); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/OnMessagePacketReceived.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnMessagePacketReceived.java new file mode 100644 index 00000000..47649a18 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnMessagePacketReceived.java @@ -0,0 +1,8 @@ +package de.thedevstack.conversationsplus.xmpp; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.xmpp.stanzas.MessagePacket; + +public interface OnMessagePacketReceived extends PacketReceived { + public void onMessagePacketReceived(Account account, MessagePacket packet); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/OnPresencePacketReceived.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnPresencePacketReceived.java new file mode 100644 index 00000000..70b0ccd2 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnPresencePacketReceived.java @@ -0,0 +1,8 @@ +package de.thedevstack.conversationsplus.xmpp; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.xmpp.stanzas.PresencePacket; + +public interface OnPresencePacketReceived extends PacketReceived { + public void onPresencePacketReceived(Account account, PresencePacket packet); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/OnStatusChanged.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnStatusChanged.java new file mode 100644 index 00000000..c8478bbb --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnStatusChanged.java @@ -0,0 +1,7 @@ +package de.thedevstack.conversationsplus.xmpp; + +import de.thedevstack.conversationsplus.entities.Account; + +public interface OnStatusChanged { + public void onStatusChanged(Account account); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/OnUpdateBlocklist.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnUpdateBlocklist.java new file mode 100644 index 00000000..ea7b8c62 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/OnUpdateBlocklist.java @@ -0,0 +1,13 @@ +package de.thedevstack.conversationsplus.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/de/thedevstack/conversationsplus/xmpp/PacketReceived.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/PacketReceived.java new file mode 100644 index 00000000..6274aaa2 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/PacketReceived.java @@ -0,0 +1,5 @@ +package de.thedevstack.conversationsplus.xmpp; + +public abstract interface PacketReceived { + +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/XmppConnection.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/XmppConnection.java new file mode 100644 index 00000000..039d8375 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/XmppConnection.java @@ -0,0 +1,1125 @@ +package de.thedevstack.conversationsplus.xmpp; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.Parcelable; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.os.SystemClock; +import android.preference.PreferenceManager; +import android.util.Log; +import android.util.Pair; +import android.util.SparseArray; + +import org.apache.http.conn.ssl.StrictHostnameVerifier; +import org.json.JSONException; +import org.json.JSONObject; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.net.ConnectException; +import java.net.IDN; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.X509TrustManager; + +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.crypto.sasl.DigestMd5; +import de.thedevstack.conversationsplus.crypto.sasl.Plain; +import de.thedevstack.conversationsplus.crypto.sasl.SaslMechanism; +import de.thedevstack.conversationsplus.crypto.sasl.ScramSha1; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.generator.IqGenerator; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.utils.CryptoHelper; +import de.thedevstack.conversationsplus.utils.DNSHelper; +import de.thedevstack.conversationsplus.utils.Xmlns; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xml.Tag; +import de.thedevstack.conversationsplus.xml.TagWriter; +import de.thedevstack.conversationsplus.xml.XmlReader; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.xmpp.jingle.OnJinglePacketReceived; +import de.thedevstack.conversationsplus.xmpp.jingle.stanzas.JinglePacket; +import de.thedevstack.conversationsplus.xmpp.stanzas.AbstractStanza; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; +import de.thedevstack.conversationsplus.xmpp.stanzas.MessagePacket; +import de.thedevstack.conversationsplus.xmpp.stanzas.PresencePacket; +import de.thedevstack.conversationsplus.xmpp.stanzas.csi.ActivePacket; +import de.thedevstack.conversationsplus.xmpp.stanzas.csi.InactivePacket; +import de.thedevstack.conversationsplus.xmpp.stanzas.streammgmt.AckPacket; +import de.thedevstack.conversationsplus.xmpp.stanzas.streammgmt.EnablePacket; +import de.thedevstack.conversationsplus.xmpp.stanzas.streammgmt.RequestPacket; +import de.thedevstack.conversationsplus.xmpp.stanzas.streammgmt.ResumePacket; + +public class XmppConnection implements Runnable { + + private static final int PACKET_IQ = 0; + private static final int PACKET_MESSAGE = 1; + private static final int PACKET_PRESENCE = 2; + private final Context applicationContext; + 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 shouldBind = true; + private boolean shouldAuthenticate = true; + private Element streamFeatures; + private final HashMap<String, List<String>> disco = new HashMap<>(); + + private String streamId = null; + private int smVersion = 3; + private final SparseArray<String> messageReceipts = 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 int attempt = 0; + private final Map<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; + + public XmppConnection(final Account account, final XmppConnectionService service) { + this.account = account; + this.wakeLock = service.getPowerManager().newWakeLock( + PowerManager.PARTIAL_WAKE_LOCK, account.getJid().toBareJid().toString()); + tagWriter = new TagWriter(); + mXmppConnectionService = service; + applicationContext = service.getApplicationContext(); + } + + 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); + } + } + } + + protected void connect() { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": connecting"); + features.encryptionEnabled = false; + lastConnect = SystemClock.elapsedRealtime(); + lastPingSent = SystemClock.elapsedRealtime(); + this.attempt++; + try { + shouldAuthenticate = shouldBind = !account.isOptionSet(Account.OPTION_REGISTER); + tagReader = new XmlReader(wakeLock); + tagWriter = new TagWriter(); + packetCallbacks.clear(); + this.changeStatus(Account.State.CONNECTING); + final Bundle result = DNSHelper.getSRVRecord(account.getServer()); + final ArrayList<Parcelable> values = result.getParcelableArrayList("values"); + if ("timeout".equals(result.getString("error"))) { + throw new IOException("timeout in dns"); + } else if (values != null) { + int i = 0; + boolean socketError = true; + while (socketError && values.size() > i) { + final Bundle namePort = (Bundle) values.get(i); + try { + String srvRecordServer; + try { + srvRecordServer=IDN.toASCII(namePort.getString("name")); + } catch (final IllegalArgumentException e) { + // TODO: Handle me?` + srvRecordServer = ""; + } + final int srvRecordPort = namePort.getInt("port"); + final String srvIpServer = namePort.getString("ip"); + final InetSocketAddress addr; + if (srvIpServer != null) { + addr = new InetSocketAddress(srvIpServer, srvRecordPort); + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + + ": using values from dns " + srvRecordServer + + "[" + srvIpServer + "]:" + srvRecordPort); + } else { + addr = new InetSocketAddress(srvRecordServer, srvRecordPort); + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + + ": using values from dns " + + srvRecordServer + ":" + srvRecordPort); + } + socket = new Socket(); + socket.connect(addr, 20000); + socketError = false; + } catch (final UnknownHostException e) { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); + i++; + } catch (final IOException e) { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); + i++; + } + } + if (socketError) { + throw new UnknownHostException(); + } + } else if (result.containsKey("error") + && "nosrv".equals(result.getString("error", null))) { + socket = new Socket(account.getServer().getDomainpart(), 5222); + } else { + throw new IOException("timeout in dns"); + } + final OutputStream out = socket.getOutputStream(); + tagWriter.setOutputStream(out); + final InputStream in = socket.getInputStream(); + tagReader.setInputStream(in); + tagWriter.beginDocument(); + sendStartStream(); + Tag nextTag; + while ((nextTag = tagReader.readTag()) != null) { + if (nextTag.isStart("stream")) { + processStream(nextTag); + break; + } else { + throw new IOException("unknown tag on connect"); + } + } + if (socket.isConnected()) { + socket.close(); + } + } catch (final UnknownHostException | ConnectException e) { + this.changeStatus(Account.State.SERVER_NOT_FOUND); + } catch (final IOException | XmlPullParserException | NoSuchAlgorithmException e) { + Log.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 { + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (final RuntimeException ignored) { + } + } + } + } + + @Override + public void run() { + try { + if (socket != null) { + socket.close(); + } + } catch (final IOException ignored) { + + } + connect(); + } + + private void processStream(final Tag currentTag) throws XmlPullParserException, + IOException, NoSuchAlgorithmException { + Tag nextTag = tagReader.readTag(); + + while ((nextTag != null) && (!nextTag.isEnd("stream"))) { + if (nextTag.isStart("error")) { + processStreamError(nextTag); + } else if (nextTag.isStart("features")) { + processStreamFeatures(nextTag); + } else if (nextTag.isStart("proceed")) { + switchOverToTls(nextTag); + } else if (nextTag.isStart("success")) { + final String challenge = tagReader.readElement(nextTag).getContent(); + try { + saslMechanism.getResponse(challenge); + } catch (final SaslMechanism.AuthenticationException e) { + disconnect(true); + Log.e(Config.LOGTAG, String.valueOf(e)); + } + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": logged in"); + account.setKey(Account.PINNED_MECHANISM_KEY, + String.valueOf(saslMechanism.getPriority())); + tagReader.reset(); + sendStartStream(); + processStream(tagReader.readTag()); + break; + } else if (nextTag.isStart("failure")) { + tagReader.readElement(nextTag); + changeStatus(Account.State.UNAUTHORIZED); + } else if (nextTag.isStart("challenge")) { + final String challenge = tagReader.readElement(nextTag).getContent(); + final Element response = new Element("response"); + response.setAttribute("xmlns", + "urn:ietf:params:xml:ns:xmpp-sasl"); + try { + response.setContent(saslMechanism.getResponse(challenge)); + } catch (final SaslMechanism.AuthenticationException e) { + // TODO: Send auth abort tag. + Log.e(Config.LOGTAG, e.toString()); + } + tagWriter.writeElement(response); + } else if (nextTag.isStart("enabled")) { + 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 managment(" + smVersion + ") enabled"); + } + this.lastSessionStarted = SystemClock.elapsedRealtime(); + 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"); + } + if (acknowledgedListener != null) { + for (int i = 0; i < messageReceipts.size(); ++i) { + if (serverCount >= messageReceipts.keyAt(i)) { + acknowledgedListener.onMessageAcknowledged( + account, messageReceipts.valueAt(i)); + } + } + } + messageReceipts.clear(); + } catch (final NumberFormatException ignored) { + } + sendServiceDiscoveryInfo(account.getServer()); + sendServiceDiscoveryItems(account.getServer()); + sendInitialPing(); + } else if (nextTag.isStart("r")) { + tagReader.readElement(nextTag); + 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(); + final int serverSequence = Integer.parseInt(ack.getAttribute("h")); + final String msgId = this.messageReceipts.get(serverSequence); + if (msgId != null) { + if (this.acknowledgedListener != null) { + this.acknowledgedListener.onMessageAcknowledged( + account, msgId); + } + this.messageReceipts.remove(serverSequence); + } + } else if (nextTag.isStart("failed")) { + tagReader.readElement(nextTag); + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": resumption failed"); + streamId = null; + if (account.getStatus() != Account.State.ONLINE) { + sendBindRequest(); + } + } else if (nextTag.isStart("iq")) { + processIq(nextTag); + } else if (nextTag.isStart("message")) { + processMessage(nextTag); + } else if (nextTag.isStart("presence")) { + processPresence(nextTag); + } + nextTag = tagReader.readTag(); + } + if (account.getStatus() == Account.State.ONLINE) { + account. setStatus(Account.State.OFFLINE); + if (statusListener != null) { + statusListener.onStatusChanged(account); + } + } + } + + private void sendInitialPing() { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": sending intial ping"); + final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); + iq.setFrom(account.getJid()); + iq.addChild("ping", "urn:xmpp:ping"); + this.sendIqPacket(iq, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(final Account account, final IqPacket packet) { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + + ": online with resource " + account.getResource()); + changeStatus(Account.State.ONLINE); + } + }); + } + + 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"); + } + } + ++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 { + 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)) { + packetCallbackDuple.second.onIqPacketReceived(account, packet); + 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())) { + packetCallbackDuple.second.onIqPacketReceived(account, packet); + packetCallbacks.remove(packet.getId()); + } else { + Log.e(Config.LOGTAG,account.getJid().toBareJid().toString()+": ignoring spoofed iq packet"); + } + } + } else if (packet.getType() == IqPacket.TYPE.GET|| packet.getType() == IqPacket.TYPE.SET) { + this.unregisteredIqListener.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 SharedPreferences getPreferences() { + return PreferenceManager.getDefaultSharedPreferences(applicationContext); + } + + private boolean enableLegacySSL() { + return getPreferences().getBoolean("enable_legacy_ssl", false); + } + + private void switchOverToTls(final Tag currentTag) throws XmlPullParserException, IOException { + tagReader.readTag(); + try { + final SSLContext sc = SSLContext.getInstance("TLS"); + sc.init(null,new X509TrustManager[]{this.mXmppConnectionService.getMemorizingTrustManager()},mXmppConnectionService.getRNG()); + final SSLSocketFactory factory = sc.getSocketFactory(); + final HostnameVerifier verifier = this.mXmppConnectionService.getMemorizingTrustManager().wrapHostnameVerifier(new StrictHostnameVerifier()); + final InetAddress address = socket == null ? null : socket.getInetAddress(); + + if (factory == null || address == null || verifier == null) { + throw new IOException("could not setup ssl"); + } + + final SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,address.getHostAddress(), socket.getPort(),true); + + if (sslSocket == null) { + throw new IOException("could not initialize ssl socket"); + } + + final String[] supportProtocols; + final Collection<String> supportedProtocols = new LinkedList<>( + Arrays.asList(sslSocket.getSupportedProtocols())); + supportedProtocols.remove("SSLv3"); + supportProtocols = supportedProtocols.toArray(new String[supportedProtocols.size()]); + + sslSocket.setEnabledProtocols(supportProtocols); + + final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites( + sslSocket.getSupportedCipherSuites()); + //Log.d(Config.LOGTAG, "Using ciphers: " + Arrays.toString(cipherSuites)); + if (cipherSuites.length > 0) { + sslSocket.setEnabledCipherSuites(cipherSuites); + } + + if (!verifier.verify(account.getServer().getDomainpart(),sslSocket.getSession())) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": TLS certificate verification failed"); + disconnect(true); + changeStatus(Account.State.SECURITY_ERROR); + } + tagReader.setInputStream(sslSocket.getInputStream()); + tagWriter.setOutputStream(sslSocket.getOutputStream()); + sendStartStream(); + Log.d(Config.LOGTAG, account.getJid().toBareJid()+ ": TLS connection established"); + features.encryptionEnabled = true; + processStream(tagReader.readTag()); + sslSocket.close(); + } catch (final NoSuchAlgorithmException | KeyManagementException e1) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": TLS certificate verification failed"); + disconnect(true); + changeStatus(Account.State.SECURITY_ERROR); + } + } + + 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) + && features.encryptionEnabled) { + sendRegistryRequest(); + } 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) { + 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("SCRAM-SHA-1")) { + saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG()); + } else if (mechanisms.contains("PLAIN")) { + saslMechanism = new Plain(tagWriter, account); + } else if (mechanisms.contains("DIGEST-MD5")) { + saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG()); + } + final JSONObject keys = account.getKeys(); + try { + if (keys.has(Account.PINNED_MECHANISM_KEY) && + keys.getInt(Account.PINNED_MECHANISM_KEY) > saslMechanism.getPriority() ) { + Log.e(Config.LOGTAG, "Auth failed. Authentication mechanism " + saslMechanism.getMechanism() + + " has lower priority (" + String.valueOf(saslMechanism.getPriority()) + + ") than pinned priority (" + keys.getInt(Account.PINNED_MECHANISM_KEY) + + "). Possible downgrade attack?"); + disconnect(true); + changeStatus(Account.State.SECURITY_ERROR); + } + } catch (final JSONException e) { + Log.d(Config.LOGTAG, "Parse error while checking pinned auth mechanism"); + } + Log.d(Config.LOGTAG,account.getJid().toString()+": Authenticating with " + saslMechanism.getMechanism()); + auth.setAttribute("mechanism", saslMechanism.getMechanism()); + if (!saslMechanism.getClientFirstMessage().isEmpty()) { + auth.setContent(saslMechanism.getClientFirstMessage()); + } + tagWriter.writeElement(auth); + } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" + + smVersion) + && streamId != null) { + final ResumePacket resume = new ResumePacket(this.streamId, + stanzasReceived, smVersion); + this.tagWriter.writeStanzaAsync(resume); + } else if (this.streamFeatures.hasChild("bind") && shouldBind) { + sendBindRequest(); + } else { + disconnect(true); + changeStatus(Account.State.INCOMPATIBLE_SERVER); + } + } + + 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; + } + + 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) { + final Element instructions = packet.query().findChild("instructions"); + if (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, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(final Account account, final 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); + } + }); + } else { + changeStatus(Account.State.REGISTRATION_FAILED); + disconnect(true); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + + ": could not register. instructions are" + + instructions.getContent()); + } + } + }); + } + + private void sendBindRequest() { + while(!mXmppConnectionService.areMessagesInitialized()) { + try { + Thread.sleep(500); + } catch (final InterruptedException ignored) { + } + } + 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) { + final Element bind = packet.findChild("bind"); + if (bind != null) { + 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 { + disconnect(true); + } + } else { + disconnect(true); + } + } + }); + } + + 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 { + 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; + messageReceipts.clear(); + } + features.carbonsEnabled = false; + features.blockListRequested = false; + disco.clear(); + sendServiceDiscoveryInfo(account.getServer()); + sendServiceDiscoveryItems(account.getServer()); + if (bindListener != null) { + bindListener.onBind(account); + } + sendInitialPing(); + } + + private void sendServiceDiscoveryInfo(final Jid server) { + if (disco.containsKey(server.toDomainJid().toString())) { + if (account.getServer().equals(server.toDomainJid())) { + enableAdvancedStreamFeatures(); + } + } else { + final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); + iq.setTo(server.toDomainJid()); + iq.query("http://jabber.org/protocol/disco#info"); + this.sendIqPacket(iq, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(final Account account, final IqPacket packet) { + final List<Element> elements = packet.query().getChildren(); + final List<String> features = new ArrayList<>(); + for (final Element element : elements) { + if (element.getName().equals("identity")) { + if ("irc".equals(element.getAttribute("type"))) { + //add fake feature to not confuse irc and real muc + features.add("siacs:no:muc"); + } + } else if (element.getName().equals("feature")) { + features.add(element.getAttribute("var")); + } + } + disco.put(server.toDomainJid().toString(), features); + + if (account.getServer().equals(server.toDomainJid())) { + enableAdvancedStreamFeatures(); + for (final OnAdvancedStreamFeaturesLoaded listener : advancedStreamFeaturesLoadedListeners) { + listener.onAdvancedStreamFeaturesAvailable(account); + } + } + } + }); + } + } + + private void enableAdvancedStreamFeatures() { + if (getFeatures().carbons() && !features.carbonsEnabled) { + sendEnableCarbons(); + } + if (getFeatures().blocking() && !features.blockListRequested) { + Log.d(Config.LOGTAG, "Requesting block list"); + this.sendIqPacket(getIqGenerator().generateGetBlockList(), mXmppConnectionService.getIqParser()); + } + } + + private void sendServiceDiscoveryItems(final Jid server) { + final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); + iq.setTo(server.toDomainJid()); + iq.query("http://jabber.org/protocol/disco#items"); + this.sendIqPacket(iq, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(final Account account, final IqPacket packet) { + 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); + } + } + } + } + }); + } + + 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")) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + + ": successfully enabled carbons"); + features.carbonsEnabled = true; + } else { + Log.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()); + Log.d(Config.LOGTAG, + account.getJid().toBareJid() + ": switching resource due to conflict (" + + account.getResource() + ")"); + } + } + + private void sendStartStream() throws IOException { + final Tag stream = Tag.start("stream:stream"); + stream.setAttribute("from", account.getJid().toBareJid().toString()); + stream.setAttribute("to", account.getServer().toString()); + stream.setAttribute("version", "1.0"); + stream.setAttribute("xml:lang", "en"); + stream.setAttribute("xmlns", "jabber:client"); + stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams"); + tagWriter.writeTag(stream); + } + + private String nextRandomId() { + return new BigInteger(50, mXmppConnectionService.getRNG()).toString(32); + } + + public void sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) { + packet.setFrom(account.getJid()); + this.sendUnmodifiedIqPacket(packet,callback); + + } + + private synchronized void sendUnmodifiedIqPacket(final IqPacket packet, final OnIqPacketReceived callback) { + if (packet.getId() == null) { + final String id = nextRandomId(); + packet.setAttribute("id", id); + } + if (callback != null) { + if (packet.getId() == null) { + packet.setId(nextRandomId()); + } + packetCallbacks.put(packet.getId(), new Pair<>(packet, callback)); + } + this.sendPacket(packet); + } + + 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) { + final String name = packet.getName(); + if (name.equals("iq") || name.equals("message") || name.equals("presence")) { + ++stanzasSent; + } + tagWriter.writeStanzaAsync(packet); + if (packet instanceof MessagePacket && packet.getId() != null && this.streamId != null) { + Log.d(Config.LOGTAG, "request delivery report for stanza " + stanzasSent); + this.messageReceipts.put(stanzasSent, packet.getId()); + tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion)); + } + } + + public void sendPing() { + if (streamFeatures.hasChild("sm")) { + tagWriter.writeStanzaAsync(new RequestPacket(smVersion)); + } else { + 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 disconnect(final boolean force) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": disconnecting"); + try { + if (force) { + socket.close(); + return; + } + new Thread(new Runnable() { + + @Override + public void run() { + if (tagWriter.isActive()) { + tagWriter.finish(); + try { + while (!tagWriter.finished()) { + Log.d(Config.LOGTAG, "not yet finished"); + Thread.sleep(100); + } + tagWriter.writeTag(Tag.end("stream:stream")); + socket.close(); + } catch (final IOException e) { + Log.d(Config.LOGTAG, + "io exception during disconnect"); + } catch (final InterruptedException e) { + Log.d(Config.LOGTAG, "interrupted"); + } + } + } + }).start(); + } catch (final IOException e) { + Log.d(Config.LOGTAG, "io exception during disconnect"); + } + } + + public List<String> findDiscoItemsByFeature(final String feature) { + final List<String> items = new ArrayList<>(); + for (final Entry<String, List<String>> cursor : disco.entrySet()) { + if (cursor.getValue().contains(feature)) { + items.add(cursor.getKey()); + } + } + return items; + } + + public String findDiscoItemByFeature(final String feature) { + final List<String> items = findDiscoItemsByFeature(feature); + if (items.size() >= 1) { + return items.get(0); + } + return null; + } + + public void r() { + this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion)); + } + + public String getMucServer() { + for (final Entry<String, List<String>> cursor : disco.entrySet()) { + final List<String> value = cursor.getValue(); + if (value.contains("http://jabber.org/protocol/muc") && !value.contains("jabber:iq:gateway") && !value.contains("siacs:no:muc")) { + return cursor.getKey(); + } + } + 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 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 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) { + return connection.disco.containsKey(server.toDomainJid().toString()) && + connection.disco.get(server.toDomainJid().toString()).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; + } + + public boolean csi() { + return connection.streamFeatures != null && connection.streamFeatures.hasChild("csi", "urn:xmpp:csi:0"); + } + + public boolean pubsub() { + return hasDiscoFeature(account.getServer(), + "http://jabber.org/protocol/pubsub#publish"); + } + + public boolean mam() { + return hasDiscoFeature(account.getServer(), "urn:xmpp:mam:0"); + } + + public boolean advancedStreamFeaturesLoaded() { + return disco.containsKey(account.getServer().toString()); + } + + public boolean rosterVersioning() { + return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver"); + } + + public void setBlockListRequested(boolean value) { + this.blockListRequested = value; + } + } + + private IqGenerator getIqGenerator() { + return mXmppConnectionService.getIqGenerator(); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/chatstate/ChatState.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/chatstate/ChatState.java new file mode 100644 index 00000000..929d485a --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/chatstate/ChatState.java @@ -0,0 +1,32 @@ +package de.thedevstack.conversationsplus.xmpp.chatstate; + +import de.thedevstack.conversationsplus.xml.Element; + +public enum ChatState { + + ACTIVE, INACTIVE, GONE, COMPOSING, PAUSED, mIncomingChatState; + + 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/de/thedevstack/conversationsplus/xmpp/forms/Data.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/forms/Data.java new file mode 100644 index 00000000..591d8ada --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/forms/Data.java @@ -0,0 +1,85 @@ +package de.thedevstack.conversationsplus.xmpp.forms; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import de.thedevstack.conversationsplus.xml.Element; + +public class Data extends Element { + + 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")) { + 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"); + removeNonFieldChildren(); + for(Field field : getFields()) { + field.removeNonValueChildren(); + } + } + + private void removeNonFieldChildren() { + for(Iterator<Element> iterator = this.children.iterator(); iterator.hasNext();) { + Element element = iterator.next(); + if (!element.getName().equals("field")) { + 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() { + return this.getAttribute("FORM_TYPE"); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/forms/Field.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/forms/Field.java new file mode 100644 index 00000000..9eb2d08d --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/forms/Field.java @@ -0,0 +1,50 @@ +package de.thedevstack.conversationsplus.xmpp.forms; + +import java.util.Collection; +import java.util.Iterator; + +import de.thedevstack.conversationsplus.xml.Element; + +public class Field extends Element { + + public Field(String name) { + super("field"); + this.setAttribute("var",name); + } + + private Field() { + super("field"); + } + + public String getName() { + 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; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jid/InvalidJidException.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jid/InvalidJidException.java new file mode 100644 index 00000000..3c8dd717 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jid/InvalidJidException.java @@ -0,0 +1,49 @@ +package de.thedevstack.conversationsplus.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/de/thedevstack/conversationsplus/xmpp/jid/Jid.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jid/Jid.java new file mode 100644 index 00000000..43b0e878 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jid/Jid.java @@ -0,0 +1,219 @@ +package de.thedevstack.conversationsplus.xmpp.jid; + +import android.util.LruCache; + +import net.java.otr4j.session.SessionID; + +import java.net.IDN; + +import de.thedevstack.conversationsplus.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); + } + dp = IDN.toUnicode(jid.substring(domainpartStart, slashLoc), IDN.USE_STD3_ASCII_RULES); + finaljid = finaljid + dp + "/" + rp; + } else { + resourcepart = ""; + dp = IDN.toUnicode(jid.substring(domainpartStart, jid.length()), + IDN.USE_STD3_ASCII_RULES); + 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. + return null; + } + } + + public Jid toDomainJid() { + try { + return resourcepart.isEmpty() && localpart.isEmpty() ? this : fromString(getDomainpart()); + } catch (final InvalidJidException e) { + // This should never happen. + return null; + } + } + + @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/de/thedevstack/conversationsplus/xmpp/jingle/JingleCandidate.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleCandidate.java new file mode 100644 index 00000000..bf282293 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleCandidate.java @@ -0,0 +1,147 @@ +package de.thedevstack.conversationsplus.xmpp.jingle; + +import java.util.ArrayList; +import java.util.List; + +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.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/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnection.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnection.java new file mode 100644 index 00000000..6348ec7e --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnection.java @@ -0,0 +1,971 @@ +package de.thedevstack.conversationsplus.xmpp.jingle; + +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; + +import android.content.Intent; +import android.net.Uri; +import android.os.SystemClock; +import android.util.Log; +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.Downloadable; +import de.thedevstack.conversationsplus.entities.DownloadableFile; +import de.thedevstack.conversationsplus.entities.DownloadablePlaceholder; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.xmpp.jingle.stanzas.Content; +import de.thedevstack.conversationsplus.xmpp.jingle.stanzas.JinglePacket; +import de.thedevstack.conversationsplus.xmpp.jingle.stanzas.Reason; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; + +public class JingleConnection implements Downloadable { + + 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 = 4096; + + private int mJingleStatus = -1; + private int mStatus = Downloadable.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 long mLastGuiRefresh = 0; + + private boolean receivedCandidate = false; + private boolean sentCandidate = false; + + private boolean acceptedAutomatically = false; + + private JingleTransport transport = null; + + private OnIqPacketReceived responseListener = new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.ERROR) { + fail(); + } + } + }; + + final OnFileTransmissionStatusChanged onFileTransmissionSatusChanged = new OnFileTransmissionStatusChanged() { + + @Override + public void onFileTransmitted(DownloadableFile file) { + if (responder.equals(account.getJid())) { + sendSuccess(); + if (acceptedAutomatically) { + message.markUnread(); + JingleConnection.this.mXmppConnectionService + .getNotificationService().push(message); + } + mXmppConnectionService.getFileBackend().updateFileParams(message); + mXmppConnectionService.databaseBackend.createMessage(message); + mXmppConnectionService.markMessage(message, + Message.STATUS_RECEIVED); + } else { + if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + file.delete(); + } + } + Log.d(Config.LOGTAG,"sucessfully transmitted file:" + file.getAbsolutePath()); + if (message.getEncryption() != Message.ENCRYPTION_PGP) { + Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + intent.setData(Uri.fromFile(file)); + mXmppConnectionService.sendBroadcast(intent); + } + } + + @Override + public void onFileTransferAborted() { + JingleConnection.this.sendCancel(); + JingleConnection.this.fail(); + } + }; + + private OnProxyActivated onProxyActivated = new OnProxyActivated() { + + @Override + public void success() { + if (initiator.equals(account.getJid())) { + Log.d(Config.LOGTAG, "we were initiating. sending file"); + transport.send(file, onFileTransmissionSatusChanged); + } else { + transport.receive(file, onFileTransmissionSatusChanged); + Log.d(Config.LOGTAG, "we were responding. receiving file"); + } + } + + @Override + public void failed() { + Log.d(Config.LOGTAG, "proxy activation failed"); + } + }; + + public JingleConnection(JingleConnectionManager mJingleConnectionManager) { + this.mJingleConnectionManager = mJingleConnectionManager; + this.mXmppConnectionService = mJingleConnectionManager + .getXmppConnectionService(); + } + + 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; + Log.d(Config.LOGTAG, "trying to fallback to something unknown" + + packet.toString()); + } + } else if (packet.isAction("transport-accept")) { + returnResult = this.receiveTransportAccept(packet); + } else { + Log.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); + } + account.getXmppConnection().sendIqPacket(response, null); + } + + public void init(Message message) { + this.contentCreator = "initiator"; + this.contentName = this.mJingleConnectionManager.nextRandomId(); + this.message = message; + this.message.setDownloadable(this); + this.mStatus = Downloadable.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() { + Log.d(Config.LOGTAG, + "connection to our own primary candidete failed"); + sendInitRequest(); + } + + @Override + public void established() { + Log.d(Config.LOGTAG, + "succesfully connected to our own primary candidate"); + mergeCandidate(candidate); + sendInitRequest(); + } + }); + mergeCandidate(candidate); + } else { + Log.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 = Downloadable.STATUS_OFFER; + this.message.setDownloadable(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(); + if (fileOffer != null) { + Element fileSize = fileOffer.findChild("size"); + Element fileNameElement = fileOffer.findChild("name"); + if (fileNameElement != null) { + String[] filename = fileNameElement.getContent() + .toLowerCase(Locale.US).toLowerCase().split("\\."); + String extension = filename[filename.length - 1]; + if (Arrays.asList(VALID_IMAGE_EXTENSIONS).contains(extension)) { + message.setType(Message.TYPE_IMAGE); + message.setRelativeFilePath(message.getUuid()+"."+extension); + } else if (Arrays.asList(VALID_CRYPTO_EXTENSIONS).contains( + filename[filename.length - 1])) { + if (filename.length == 3) { + extension = filename[filename.length - 2]; + if (Arrays.asList(VALID_IMAGE_EXTENSIONS).contains(extension)) { + message.setType(Message.TYPE_IMAGE); + message.setRelativeFilePath(message.getUuid()+"."+extension); + } else { + message.setType(Message.TYPE_FILE); + } + if (filename[filename.length - 1].equals("otr")) { + message.setEncryption(Message.ENCRYPTION_OTR); + } else { + message.setEncryption(Message.ENCRYPTION_PGP); + } + } + } else { + message.setType(Message.TYPE_FILE); + } + if (message.getType() == Message.TYPE_FILE) { + String suffix = ""; + if (!fileNameElement.getContent().isEmpty()) { + String parts[] = fileNameElement.getContent().split("/"); + suffix = parts[parts.length - 1]; + if (message.getEncryption() == Message.ENCRYPTION_OTR && suffix.endsWith(".otr")) { + suffix = suffix.substring(0,suffix.length() - 4); + } else if (message.getEncryption() == Message.ENCRYPTION_PGP && (suffix.endsWith(".pgp") || suffix.endsWith(".gpg"))) { + suffix = suffix.substring(0,suffix.length() - 4); + } + } + message.setRelativeFilePath(message.getUuid()+"_"+suffix); + } + long size = Long.parseLong(fileSize.getContent()); + message.setBody(Long.toString(size)); + conversation.add(message); + mXmppConnectionService.updateConversationUi(); + if (size <= this.mJingleConnectionManager.getAutoAcceptFileSize() + && mXmppConnectionService.isDownloadAllowedInConnection()) { + Log.d(Config.LOGTAG, "auto accepting file from " + + packet.getFrom()); + this.acceptedAutomatically = true; + this.sendAccept(); + } else { + message.markUnread(); + Log.d(Config.LOGTAG, + "not auto accepting new file offer with size: " + + size + + " allowed size:" + + this.mJingleConnectionManager + .getAutoAcceptFileSize()); + this.mXmppConnectionService.getNotificationService() + .push(message); + } + this.file = this.mXmppConnectionService.getFileBackend() + .getFile(message, false); + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + byte[] key = conversation.getSymmetricKey(); + if (key == null) { + this.sendCancel(); + this.fail(); + return; + } else { + this.file.setKey(key); + } + } + this.file.setExpectedSize(size); + } 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 = this.mXmppConnectionService.getFileBackend().getFile( + message, false); + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + Conversation conversation = this.message.getConversation(); + this.mXmppConnectionService.renewSymmetricKey(conversation); + content.setFileOffer(this.file, true); + this.file.setKey(conversation.getSymmetricKey()); + } else { + content.setFileOffer(this.file, false); + } + 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.ERROR) { + mJingleStatus = JINGLE_STATUS_INITIATED; + mXmppConnectionService.markMessage(message, Message.STATUS_OFFERED); + } else { + fail(); + } + } + }); + + } + } + + private List<Element> getCandidatesAsElements() { + List<Element> elements = new ArrayList<>(); + for (JingleCandidate c : this.candidates) { + elements.add(c.toElement()); + } + return elements; + } + + private void sendAccept() { + mJingleStatus = JINGLE_STATUS_ACCEPTED; + this.mStatus = Downloadable.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() { + Log.d(Config.LOGTAG,"connection to our own primary candidate failed"); + content.socks5transport().setChildren(getCandidatesAsElements()); + packet.setContent(content); + sendJinglePacket(packet); + connectNextCandidate(); + } + + @Override + public void established() { + Log.d(Config.LOGTAG, "connected to primary candidate"); + mergeCandidate(candidate); + content.socks5transport().setChildren(getCandidatesAsElements()); + packet.setContent(content); + sendJinglePacket(packet); + connectNextCandidate(); + } + }); + } else { + Log.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) { + account.getXmppConnection().sendIqPacket(packet, responseListener); + } + + private void sendJinglePacket(JinglePacket packet, OnIqPacketReceived callback) { + account.getXmppConnection().sendIqPacket(packet,callback); + } + + private boolean receiveAccept(JinglePacket packet) { + Content content = packet.getJingleContent(); + mergeCandidates(JingleCandidate.parse(content.socks5transport() + .getChildren())); + this.mJingleStatus = JINGLE_STATUS_ACCEPTED; + mXmppConnectionService.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"); + Log.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 { + Log.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")) { + Log.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) { + Log.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 { + Log.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) { + Log.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()) { + Log.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()); + this.account.getXmppConnection().sendIqPacket(activation, + new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, + IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.ERROR) { + onProxyActivated.failed(); + } else { + onProxyActivated.success(); + sendProxyActivated(connection + .getCandidate().getCid()); + } + } + }); + } else { + Log.d(Config.LOGTAG, + "candidate " + + connection.getCandidate().getCid() + + " was a proxy. waiting for other party to activate"); + } + } else { + if (initiator.equals(account.getJid())) { + Log.d(Config.LOGTAG, "we were initiating. sending file"); + connection.send(file, onFileTransmissionSatusChanged); + } else { + Log.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(); + // Log.d(Config.LOGTAG,"comparing candidate: "+currentConnection.getCandidate().toString()); + if (currentConnection.isEstablished() + && (currentConnection.getCandidate().isUsedByCounterpart() || (!currentConnection + .getCandidate().isOurs()))) { + // Log.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()) { + // Log.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.setDownloadable(null); + this.mXmppConnectionService.updateMessage(message); + this.mJingleConnectionManager.finishConnection(this); + } + + private void sendFallbackToIbb() { + Log.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) { + Log.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", + Integer.toString(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() { + Log.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; + this.mXmppConnectionService.markMessage(this.message,Message.STATUS_SEND_RECEIVED); + this.disconnectSocks5Connections(); + if (this.transport != null && this.transport instanceof JingleInbandTransport) { + this.transport.disconnect(); + } + this.message.setDownloadable(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.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_FAILED)); + if (this.file!=null) { + file.delete(); + } + this.mXmppConnectionService.updateConversationUi(); + } else { + this.mXmppConnectionService.markMessage(this.message, + Message.STATUS_SEND_FAILED); + this.message.setDownloadable(null); + } + } + + private void fail() { + this.mJingleStatus = JINGLE_STATUS_FAILED; + this.disconnectSocks5Connections(); + if (this.transport != null && this.transport instanceof JingleInbandTransport) { + this.transport.disconnect(); + } + if (this.message != null) { + if (this.responder.equals(account.getJid())) { + this.message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_FAILED)); + if (this.file!=null) { + file.delete(); + } + this.mXmppConnectionService.updateConversationUi(); + } else { + this.mXmppConnectionService.markMessage(this.message, + Message.STATUS_SEND_FAILED); + this.message.setDownloadable(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() { + Log.d(Config.LOGTAG, + "connection failed with " + candidate.getHost() + ":" + + candidate.getPort()); + connectNextCandidate(); + } + + @Override + public void established() { + Log.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; + if (SystemClock.elapsedRealtime() - this.mLastGuiRefresh > Config.PROGRESS_UI_UPDATE_INTERVAL) { + this.mLastGuiRefresh = SystemClock.elapsedRealtime(); + 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; + } + + @Override + public String getMimeType() { + if (this.message.getType() == Message.TYPE_FILE) { + String mime = null; + String path = this.message.getRelativeFilePath(); + if (path != null && !this.message.getRelativeFilePath().isEmpty()) { + mime = URLConnection.guessContentTypeFromName(this.message.getRelativeFilePath()); + if (mime!=null) { + return mime; + } else { + return ""; + } + } else { + return ""; + } + } else { + return "image/webp"; + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnectionManager.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnectionManager.java new file mode 100644 index 00000000..eb1bfe2e --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnectionManager.java @@ -0,0 +1,164 @@ +package de.thedevstack.conversationsplus.xmpp.jingle; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import android.annotation.SuppressLint; +import android.util.Log; +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.services.AbstractConnectionManager; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.utils.Xmlns; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.xmpp.jingle.stanzas.JinglePacket; +import de.thedevstack.conversationsplus.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 JingleConnectionManager(XmppConnectionService service) { + super(service); + } + + 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) { + JingleConnection connection = new JingleConnection(this); + 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.NO_PROXY_LOOKUP) { + listener.onPrimaryCandidateFound(false, null); + return; + } + if (!this.primaryCandidates.containsKey(account.getJid().toBareJid())) { + final String proxy = account.getXmppConnection().findDiscoItemByFeature(Xmlns.BYTE_STREAMS); + if (proxy != null) { + IqPacket iq = new IqPacket(IqPacket.TYPE.GET); + iq.setAttribute("to", 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(Jid.fromString(proxy)); + candidate.setPriority(655360 + 65535); + primaryCandidates.put(account.getJid().toBareJid(),candidate); + listener.onPrimaryCandidateFound(true,candidate); + } catch (final NumberFormatException | InvalidJidException 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; + } + } + } + Log.d(Config.LOGTAG,"couldn't deliver payload: " + payload.toString()); + } else { + Log.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/de/thedevstack/conversationsplus/xmpp/jingle/JingleInbandTransport.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleInbandTransport.java new file mode 100644 index 00000000..7336eff7 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleInbandTransport.java @@ -0,0 +1,224 @@ +package de.thedevstack.conversationsplus.xmpp.jingle; + +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 android.util.Base64; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.DownloadableFile; +import de.thedevstack.conversationsplus.utils.CryptoHelper; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; + +public class JingleInbandTransport extends JingleTransport { + + private Account account; + private Jid counterpart; + private int blockSize; + private int bufferSize; + 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.bufferSize = blocksize / 4; + 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.ERROR) { + 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 = file.createOutputStream(); + if (this.fileOutputStream == null) { + callback.onFileTransferAborted(); + return; + } + this.remainingSize = this.fileSize = file.getExpectedSize(); + } catch (final NoSuchAlgorithmException | IOException e) { + callback.onFileTransferAborted(); + } + } + + @Override + public void send(DownloadableFile file, + OnFileTransmissionStatusChanged callback) { + this.onFileTransmissionStatusChanged = callback; + this.file = file; + try { + this.remainingSize = this.file.getSize(); + this.fileSize = this.remainingSize; + this.digest = MessageDigest.getInstance("SHA-1"); + this.digest.reset(); + fileInputStream = this.file.createInputStream(); + if (fileInputStream == null) { + callback.onFileTransferAborted(); + return; + } + if (this.connected) { + this.sendNextBlock(); + } + } catch (NoSuchAlgorithmException e) { + callback.onFileTransferAborted(); + } + } + + @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.bufferSize]; + try { + int count = fileInputStream.read(buffer); + if (count == -1) { + file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); + fileInputStream.close(); + this.onFileTransmissionStatusChanged.onFileTransmitted(file); + } else { + this.remainingSize -= count; + this.digest.update(buffer); + String base64 = Base64.encodeToString(buffer, 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.seq++; + connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100)); + } + } catch (IOException e) { + 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) { + this.onFileTransmissionStatusChanged.onFileTransferAborted(); + } + } + + public void deliverPayload(IqPacket packet, Element payload) { + if (payload.getName().equals("open")) { + if (!established) { + established = true; + connected = true; + 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/de/thedevstack/conversationsplus/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleSocks5Transport.java new file mode 100644 index 00000000..a1094dd7 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleSocks5Transport.java @@ -0,0 +1,234 @@ +package de.thedevstack.conversationsplus.xmpp.jingle; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import de.thedevstack.conversationsplus.entities.DownloadableFile; +import de.thedevstack.conversationsplus.utils.CryptoHelper; + +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(candidate.getHost(), + candidate.getPort()); + inputStream = socket.getInputStream(); + outputStream = socket.getOutputStream(); + byte[] login = { 0x05, 0x01, 0x00 }; + byte[] expectedReply = { 0x05, 0x00 }; + byte[] reply = new byte[2]; + outputStream.write(login); + inputStream.read(reply); + final String connect = Character.toString('\u0005') + + '\u0001' + '\u0000' + '\u0003' + '\u0028' + + destination + '\u0000' + '\u0000'; + if (Arrays.equals(reply, expectedReply)) { + outputStream.write(connect.getBytes()); + byte[] result = new byte[2]; + inputStream.read(result); + int status = result[1]; + if (status == 0) { + isEstablished = true; + callback.established(); + } else { + callback.failed(); + } + } else { + socket.close(); + callback.failed(); + } + } 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; + try { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.reset(); + fileInputStream = file.createInputStream(); + if (fileInputStream == null) { + callback.onFileTransferAborted(); + return; + } + long size = file.getSize(); + 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) { + callback.onFileTransferAborted(); + } catch (IOException e) { + callback.onFileTransferAborted(); + } catch (NoSuchAlgorithmException e) { + callback.onFileTransferAborted(); + } finally { + try { + if (fileInputStream != null) { + fileInputStream.close(); + } + } catch (IOException e) { + callback.onFileTransferAborted(); + } + } + } + }).start(); + + } + + public void receive(final DownloadableFile file, + final OnFileTransmissionStatusChanged callback) { + new Thread(new Runnable() { + + @Override + public void run() { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.reset(); + inputStream.skip(45); + socket.setSoTimeout(30000); + file.getParentFile().mkdirs(); + file.createNewFile(); + OutputStream fileOutputStream = file.createOutputStream(); + if (fileOutputStream == null) { + callback.onFileTransferAborted(); + return; + } + double size = file.getExpectedSize(); + long remainingSize = file.getExpectedSize(); + byte[] buffer = new byte[8192]; + int count = buffer.length; + while (remainingSize > 0) { + count = inputStream.read(buffer); + if (count == -1) { + callback.onFileTransferAborted(); + 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) { + callback.onFileTransferAborted(); + } catch (IOException e) { + callback.onFileTransferAborted(); + } catch (NoSuchAlgorithmException e) { + callback.onFileTransferAborted(); + } + } + }).start(); + } + + public boolean isProxy() { + return this.candidate.getType() == JingleCandidate.TYPE_PROXY; + } + + public boolean needsActivation() { + return (this.isProxy() && !this.activated); + } + + public void disconnect() { + if (this.outputStream != null) { + try { + this.outputStream.close(); + } catch (IOException e) { + + } + } + if (this.inputStream != null) { + try { + this.inputStream.close(); + } catch (IOException e) { + + } + } + if (this.socket != null) { + try { + this.socket.close(); + } catch (IOException e) { + + } + } + } + + 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/de/thedevstack/conversationsplus/xmpp/jingle/JingleTransport.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleTransport.java new file mode 100644 index 00000000..ee0fefff --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleTransport.java @@ -0,0 +1,15 @@ +package de.thedevstack.conversationsplus.xmpp.jingle; + +import de.thedevstack.conversationsplus.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/de/thedevstack/conversationsplus/xmpp/jingle/OnFileTransmissionStatusChanged.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnFileTransmissionStatusChanged.java new file mode 100644 index 00000000..86daae81 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnFileTransmissionStatusChanged.java @@ -0,0 +1,9 @@ +package de.thedevstack.conversationsplus.xmpp.jingle; + +import de.thedevstack.conversationsplus.entities.DownloadableFile; + +public interface OnFileTransmissionStatusChanged { + public void onFileTransmitted(DownloadableFile file); + + public void onFileTransferAborted(); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnJinglePacketReceived.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnJinglePacketReceived.java new file mode 100644 index 00000000..d6dda138 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnJinglePacketReceived.java @@ -0,0 +1,9 @@ +package de.thedevstack.conversationsplus.xmpp.jingle; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.xmpp.PacketReceived; +import de.thedevstack.conversationsplus.xmpp.jingle.stanzas.JinglePacket; + +public interface OnJinglePacketReceived extends PacketReceived { + public void onJinglePacketReceived(Account account, JinglePacket packet); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnPrimaryCandidateFound.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnPrimaryCandidateFound.java new file mode 100644 index 00000000..be1f1d02 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnPrimaryCandidateFound.java @@ -0,0 +1,6 @@ +package de.thedevstack.conversationsplus.xmpp.jingle; + +public interface OnPrimaryCandidateFound { + public void onPrimaryCandidateFound(boolean success, + JingleCandidate canditate); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnTransportConnected.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnTransportConnected.java new file mode 100644 index 00000000..91a79a83 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/OnTransportConnected.java @@ -0,0 +1,7 @@ +package de.thedevstack.conversationsplus.xmpp.jingle; + +public interface OnTransportConnected { + public void failed(); + + public void established(); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/stanzas/Content.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/stanzas/Content.java new file mode 100644 index 00000000..40eec6f1 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/stanzas/Content.java @@ -0,0 +1,102 @@ +package de.thedevstack.conversationsplus.xmpp.jingle.stanzas; + +import de.thedevstack.conversationsplus.entities.DownloadableFile; +import de.thedevstack.conversationsplus.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 void 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.getSize())); + if (otr) { + file.addChild("name").setContent(actualFile.getName() + ".otr"); + } else { + file.addChild("name").setContent(actualFile.getName()); + } + } + + 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/de/thedevstack/conversationsplus/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/stanzas/JinglePacket.java new file mode 100644 index 00000000..db771a0a --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/stanzas/JinglePacket.java @@ -0,0 +1,96 @@ +package de.thedevstack.conversationsplus.xmpp.jingle.stanzas; + +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.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/de/thedevstack/conversationsplus/xmpp/jingle/stanzas/Reason.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/stanzas/Reason.java new file mode 100644 index 00000000..a442d8d3 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/stanzas/Reason.java @@ -0,0 +1,13 @@ +package de.thedevstack.conversationsplus.xmpp.jingle.stanzas; + +import de.thedevstack.conversationsplus.xml.Element; + +public class Reason extends Element { + private Reason(String name) { + super(name); + } + + public Reason() { + super("reason"); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/pep/Avatar.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/pep/Avatar.java new file mode 100644 index 00000000..e2ce7174 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/pep/Avatar.java @@ -0,0 +1,73 @@ +package de.thedevstack.conversationsplus.xmpp.pep; + +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +import android.util.Base64; + +public class Avatar { + public String type; + public String sha1sum; + public String image; + public int height; + public int width; + public long size; + public Jid owner; + + public byte[] getImageAsBytes() { + return Base64.decode(image, Base64.DEFAULT); + } + + public String getFilename() { + if (type == null) { + return sha1sum; + } else if (type.equalsIgnoreCase("image/webp")) { + return sha1sum + ".webp"; + } else if (type.equalsIgnoreCase("image/png")) { + return sha1sum + ".png"; + } else { + 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"); + avatar.sha1sum = child.getAttribute("id"); + return avatar; + } + } + return null; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/AbstractStanza.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/AbstractStanza.java new file mode 100644 index 00000000..d326a214 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/AbstractStanza.java @@ -0,0 +1,54 @@ +package de.thedevstack.conversationsplus.xmpp.stanzas; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.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 String getId() { + return this.getAttribute("id"); + } + + 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 void setId(final String id) { + setAttribute("id", id); + } + + 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()); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/IqPacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/IqPacket.java new file mode 100644 index 00000000..fb08556b --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/IqPacket.java @@ -0,0 +1,63 @@ +package de.thedevstack.conversationsplus.xmpp.stanzas; + +import de.thedevstack.conversationsplus.xml.Element; + +public class IqPacket extends AbstractStanza { + + public static enum TYPE { + ERROR, + SET, + RESULT, + GET, + INVALID + } + + 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"); + switch (type) { + case "error": + return TYPE.ERROR; + case "result": + return TYPE.RESULT; + case "set": + return TYPE.SET; + case "get": + return TYPE.GET; + 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/de/thedevstack/conversationsplus/xmpp/stanzas/MessagePacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/MessagePacket.java new file mode 100644 index 00000000..db1cce54 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/MessagePacket.java @@ -0,0 +1,69 @@ +package de.thedevstack.conversationsplus.xmpp.stanzas; + +import de.thedevstack.conversationsplus.xml.Element; + +public class MessagePacket extends AbstractStanza { + 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() { + Element body = this.findChild("body"); + if (body != null) { + return body.getContent(); + } else { + return null; + } + } + + 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 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; + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/PresencePacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/PresencePacket.java new file mode 100644 index 00000000..dfe40f87 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/PresencePacket.java @@ -0,0 +1,8 @@ +package de.thedevstack.conversationsplus.xmpp.stanzas; + +public class PresencePacket extends AbstractStanza { + + public PresencePacket() { + super("presence"); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/csi/ActivePacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/csi/ActivePacket.java new file mode 100644 index 00000000..47538a64 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/csi/ActivePacket.java @@ -0,0 +1,10 @@ +package de.thedevstack.conversationsplus.xmpp.stanzas.csi; + +import de.thedevstack.conversationsplus.xmpp.stanzas.AbstractStanza; + +public class ActivePacket extends AbstractStanza { + public ActivePacket() { + super("active"); + setAttribute("xmlns", "urn:xmpp:csi:0"); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/csi/InactivePacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/csi/InactivePacket.java new file mode 100644 index 00000000..ca5904a5 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/csi/InactivePacket.java @@ -0,0 +1,10 @@ +package de.thedevstack.conversationsplus.xmpp.stanzas.csi; + +import de.thedevstack.conversationsplus.xmpp.stanzas.AbstractStanza; + +public class InactivePacket extends AbstractStanza { + public InactivePacket() { + super("inactive"); + setAttribute("xmlns", "urn:xmpp:csi:0"); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/AckPacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/AckPacket.java new file mode 100644 index 00000000..fe1740da --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/AckPacket.java @@ -0,0 +1,13 @@ +package de.thedevstack.conversationsplus.xmpp.stanzas.streammgmt; + +import de.thedevstack.conversationsplus.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/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/EnablePacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/EnablePacket.java new file mode 100644 index 00000000..ce416f27 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/EnablePacket.java @@ -0,0 +1,13 @@ +package de.thedevstack.conversationsplus.xmpp.stanzas.streammgmt; + +import de.thedevstack.conversationsplus.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/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/RequestPacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/RequestPacket.java new file mode 100644 index 00000000..eb6217ae --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/RequestPacket.java @@ -0,0 +1,12 @@ +package de.thedevstack.conversationsplus.xmpp.stanzas.streammgmt; + +import de.thedevstack.conversationsplus.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/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/ResumePacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/ResumePacket.java new file mode 100644 index 00000000..90bf566e --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/streammgmt/ResumePacket.java @@ -0,0 +1,14 @@ +package de.thedevstack.conversationsplus.xmpp.stanzas.streammgmt; + +import de.thedevstack.conversationsplus.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)); + } + +} |