aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/eu/siacs/conversations/xmpp/jingle
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/eu/siacs/conversations/xmpp/jingle')
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java143
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java902
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java168
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java191
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java212
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java13
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java9
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java9
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java6
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java7
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java102
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java95
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java13
13 files changed, 1870 insertions, 0 deletions
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java
new file mode 100644
index 00000000..3e7c7b68
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java
@@ -0,0 +1,143 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.siacs.conversations.xml.Element;
+
+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 String 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(String jid) {
+ this.jid = jid;
+ }
+
+ public String 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) {
+ if ("proxy".equals(type)) {
+ this.type = TYPE_PROXY;
+ } else if ("direct".equals(type)) {
+ this.type = TYPE_DIRECT;
+ } else {
+ this.type = TYPE_UNKNOWN;
+ }
+ }
+
+ 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.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<JingleCandidate>();
+ 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.getAttribute("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());
+ element.setAttribute("priority", Integer.toString(this.getPriority()));
+ if (this.getType() == TYPE_DIRECT) {
+ element.setAttribute("type", "direct");
+ } else if (this.getType() == TYPE_PROXY) {
+ element.setAttribute("type", "proxy");
+ }
+ return element;
+ }
+
+ public void flagAsUsedByCounterpart() {
+ this.usedByCounterpart = true;
+ }
+
+ public boolean isUsedByCounterpart() {
+ return this.usedByCounterpart;
+ }
+
+ public String toString() {
+ return this.getHost() + ":" + this.getPort() + " (prio="
+ + this.getPriority() + ")";
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
new file mode 100644
index 00000000..6b9ca9aa
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
@@ -0,0 +1,902 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+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.graphics.BitmapFactory;
+import android.net.Uri;
+import android.util.Log;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Downloadable;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
+import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class JingleConnection implements Downloadable {
+
+ private final String[] extensions = { "webp", "jpeg", "jpg", "png" };
+ private final String[] cryptoExtensions = { "pgp", "gpg", "otr" };
+
+ 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_TERMINATED = 2;
+ protected static final int JINGLE_STATUS_CANCELED = 3;
+ 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 = -1;
+ private Message message;
+ private String sessionId;
+ private Account account;
+ private String initiator;
+ private String responder;
+ private List<JingleCandidate> candidates = new ArrayList<JingleCandidate>();
+ private ConcurrentHashMap<String, JingleSocks5Transport> connections = new ConcurrentHashMap<String, JingleSocks5Transport>();
+
+ private String transportId;
+ private Element fileOffer;
+ private DownloadableFile file = null;
+
+ private String contentName;
+ private String contentCreator;
+
+ 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) {
+ cancel();
+ }
+ }
+ };
+
+ final OnFileTransmissionStatusChanged onFileTransmissionSatusChanged = new OnFileTransmissionStatusChanged() {
+
+ @Override
+ public void onFileTransmitted(DownloadableFile file) {
+ if (responder.equals(account.getFullJid())) {
+ sendSuccess();
+ if (acceptedAutomatically) {
+ message.markUnread();
+ JingleConnection.this.mXmppConnectionService
+ .getNotificationService().push(message);
+ }
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(file.getAbsolutePath(), options);
+ int imageHeight = options.outHeight;
+ int imageWidth = options.outWidth;
+ message.setBody(Long.toString(file.getSize()) + '|'
+ + imageWidth + '|' + imageHeight);
+ mXmppConnectionService.databaseBackend.createMessage(message);
+ mXmppConnectionService.markMessage(message,
+ Message.STATUS_RECEIVED);
+ }
+ 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.cancel();
+ }
+ };
+
+ private OnProxyActivated onProxyActivated = new OnProxyActivated() {
+
+ @Override
+ public void success() {
+ if (initiator.equals(account.getFullJid())) {
+ 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 String 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.cancel();
+ } else if (reason.hasChild("success")) {
+ this.receiveSuccess();
+ } else {
+ this.cancel();
+ }
+ } else {
+ this.cancel();
+ }
+ } 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.generateRespone(IqPacket.TYPE_RESULT);
+
+ } else {
+ response = packet.generateRespone(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.account = message.getConversation().getAccount();
+ this.initiator = this.account.getFullJid();
+ 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().split("/", 2)[0], false);
+ this.message = new Message(conversation, "", Message.ENCRYPTION_NONE);
+ this.message.setStatus(Message.STATUS_RECEIVED);
+ this.message.setType(Message.TYPE_IMAGE);
+ this.mStatus = Downloadable.STATUS_OFFER;
+ this.message.setDownloadable(this);
+ String[] fromParts = packet.getFrom().split("/", 2);
+ this.message.setPresence(fromParts[1]);
+ this.account = account;
+ this.initiator = packet.getFrom();
+ this.responder = this.account.getFullJid();
+ 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) {
+ boolean supportedFile = false;
+ String[] filename = fileNameElement.getContent()
+ .toLowerCase(Locale.US).split("\\.");
+ if (Arrays.asList(this.extensions).contains(
+ filename[filename.length - 1])) {
+ supportedFile = true;
+ } else if (Arrays.asList(this.cryptoExtensions).contains(
+ filename[filename.length - 1])) {
+ if (filename.length == 3) {
+ if (Arrays.asList(this.extensions).contains(
+ filename[filename.length - 2])) {
+ supportedFile = true;
+ if (filename[filename.length - 1].equals("otr")) {
+ Log.d(Config.LOGTAG, "receiving otr file");
+ this.message
+ .setEncryption(Message.ENCRYPTION_OTR);
+ } else {
+ this.message
+ .setEncryption(Message.ENCRYPTION_PGP);
+ }
+ }
+ }
+ }
+ if (supportedFile) {
+ long size = Long.parseLong(fileSize.getContent());
+ message.setBody(Long.toString(size));
+ conversation.add(message);
+ mXmppConnectionService.updateConversationUi();
+ if (size <= this.mJingleConnectionManager
+ .getAutoAcceptFileSize()) {
+ 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.cancel();
+ return;
+ } else {
+ this.file.setKey(key);
+ }
+ }
+ this.file.setExpectedSize(size);
+ } else {
+ this.sendCancel();
+ this.cancel();
+ }
+ } else {
+ this.sendCancel();
+ this.cancel();
+ }
+ } else {
+ this.sendCancel();
+ this.cancel();
+ }
+ }
+
+ private void sendInitRequest() {
+ this.mXmppConnectionService.markMessage(this.message, Message.STATUS_OFFERED);
+ JinglePacket packet = this.bootstrapPacket("session-initiate");
+ Content content = new Content(this.contentCreator, this.contentName);
+ if (message.getType() == Message.TYPE_IMAGE) {
+ 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);
+ this.mJingleStatus = JINGLE_STATUS_INITIATED;
+ }
+ }
+
+ private List<Element> getCandidatesAsElements() {
+ List<Element> elements = new ArrayList<Element>();
+ 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) && (!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.getFullJid());
+ packet.setTo(this.message.getCounterpart());
+ packet.setSessionId(this.sessionId);
+ packet.setInitiator(this.initiator);
+ return packet;
+ }
+
+ private void sendJinglePacket(JinglePacket packet) {
+ // Log.d(Config.LOGTAG,packet.toString());
+ account.getXmppConnection().sendIqPacket(packet, responseListener);
+ }
+
+ 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.cancel();
+ }
+ }
+ 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.disconnect();
+ if (this.initiator.equals(account.getFullJid())) {
+ 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());
+ 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.getFullJid())) {
+ 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.getFullJid())) {
+ 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.disconnect();
+ 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.account,
+ this.responder, 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.account,
+ this.responder, 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);
+ this.disconnect();
+ this.mJingleConnectionManager.finishConnection(this);
+ }
+
+ public void cancel() {
+ this.mJingleStatus = JINGLE_STATUS_CANCELED;
+ this.disconnect();
+ if (this.message != null) {
+ if (this.responder.equals(account.getFullJid())) {
+ this.mStatus = Downloadable.STATUS_FAILED;
+ this.mXmppConnectionService.updateConversationUi();
+ } else {
+ this.mXmppConnectionService.markMessage(this.message,
+ Message.STATUS_SEND_FAILED);
+ }
+ }
+ 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 disconnect() {
+ 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 String getInitiator() {
+ return this.initiator;
+ }
+
+ public String 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;
+ }
+
+ 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.STATUS_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;
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
new file mode 100644
index 00000000..d937146a
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
@@ -0,0 +1,168 @@
+package eu.siacs.conversations.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 eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.AbstractConnectionManager;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class JingleConnectionManager extends AbstractConnectionManager {
+ private List<JingleConnection> connections = new CopyOnWriteArrayList<JingleConnection>();
+
+ private HashMap<String, JingleCandidate> primaryCandidates = new HashMap<String, JingleCandidate>();
+
+ @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.generateRespone(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(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 (!this.primaryCandidates.containsKey(account.getJid())) {
+ String xmlns = "http://jabber.org/protocol/bytestreams";
+ final String proxy = account.getXmppConnection()
+ .findDiscoItemByFeature(xmlns);
+ if (proxy != null) {
+ IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
+ iq.setTo(proxy);
+ iq.query(xmlns);
+ account.getXmppConnection().sendIqPacket(iq,
+ new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account,
+ IqPacket packet) {
+ Element streamhost = packet
+ .query()
+ .findChild("streamhost",
+ "http://jabber.org/protocol/bytestreams");
+ if (streamhost != null) {
+ JingleCandidate candidate = new JingleCandidate(
+ nextRandomId(), true);
+ candidate.setHost(streamhost
+ .getAttribute("host"));
+ candidate.setPort(Integer
+ .parseInt(streamhost
+ .getAttribute("port")));
+ candidate
+ .setType(JingleCandidate.TYPE_PROXY);
+ candidate.setJid(proxy);
+ candidate.setPriority(655360 + 65535);
+ primaryCandidates.put(account.getJid(),
+ candidate);
+ listener.onPrimaryCandidateFound(true,
+ candidate);
+ } else {
+ listener.onPrimaryCandidateFound(false,
+ null);
+ }
+ }
+ });
+ } else {
+ listener.onPrimaryCandidateFound(false, null);
+ }
+
+ } else {
+ listener.onPrimaryCandidateFound(true,
+ this.primaryCandidates.get(account.getJid()));
+ }
+ }
+
+ 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,
+ "couldnt deliver payload: " + payload.toString());
+ } else {
+ Log.d(Config.LOGTAG, "no sid found in incomming ibb packet");
+ }
+ }
+
+ public void cancelInTransmission() {
+ for (JingleConnection connection : this.connections) {
+ if (connection.getJingleStatus() == JingleConnection.JINGLE_STATUS_TRANSMITTING) {
+ connection.cancel();
+ }
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java
new file mode 100644
index 00000000..cc1e92f6
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java
@@ -0,0 +1,191 @@
+package eu.siacs.conversations.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 eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class JingleInbandTransport extends JingleTransport {
+
+ private Account account;
+ private String counterpart;
+ private int blockSize;
+ private int bufferSize;
+ private int seq = 0;
+ private String sessionId;
+
+ private boolean established = false;
+
+ private DownloadableFile file;
+
+ private InputStream fileInputStream = null;
+ private OutputStream fileOutputStream;
+ private long remainingSize;
+ private MessageDigest digest;
+
+ private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged;
+
+ private OnIqPacketReceived onAckReceived = new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE_RESULT) {
+ sendNextBlock();
+ }
+ }
+ };
+
+ public JingleInbandTransport(Account account, String counterpart,
+ String sid, int blocksize) {
+ this.account = account;
+ this.counterpart = counterpart;
+ 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.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 = file.getExpectedSize();
+ } catch (NoSuchAlgorithmException e) {
+ callback.onFileTransferAborted();
+ } catch (IOException e) {
+ callback.onFileTransferAborted();
+ }
+ }
+
+ @Override
+ public void send(DownloadableFile file,
+ OnFileTransmissionStatusChanged callback) {
+ this.onFileTransmissionStatusChanged = callback;
+ this.file = file;
+ try {
+ this.digest = MessageDigest.getInstance("SHA-1");
+ this.digest.reset();
+ fileInputStream = this.file.createInputStream();
+ if (fileInputStream == null) {
+ callback.onFileTransferAborted();
+ return;
+ }
+ this.sendNextBlock();
+ } catch (NoSuchAlgorithmException e) {
+ callback.onFileTransferAborted();
+ }
+ }
+
+ 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.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++;
+ }
+ } 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);
+ }
+ } catch (IOException e) {
+ this.onFileTransmissionStatusChanged.onFileTransferAborted();
+ }
+ }
+
+ public void deliverPayload(IqPacket packet, Element payload) {
+ if (payload.getName().equals("open")) {
+ if (!established) {
+ established = true;
+ this.account.getXmppConnection().sendIqPacket(
+ packet.generateRespone(IqPacket.TYPE_RESULT), null);
+ } else {
+ this.account.getXmppConnection().sendIqPacket(
+ packet.generateRespone(IqPacket.TYPE_ERROR), null);
+ }
+ } else if (payload.getName().equals("data")) {
+ this.receiveNextBlock(payload.getContent());
+ this.account.getXmppConnection().sendIqPacket(
+ packet.generateRespone(IqPacket.TYPE_RESULT), null);
+ } else {
+ // TODO some sort of exception
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java
new file mode 100644
index 00000000..1da2f0cd
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java
@@ -0,0 +1,212 @@
+package eu.siacs.conversations.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 eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.utils.CryptoHelper;
+
+public class JingleSocks5Transport extends JingleTransport {
+ private JingleCandidate candidate;
+ 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;
+ try {
+ MessageDigest mDigest = MessageDigest.getInstance("SHA-1");
+ StringBuilder destBuilder = new StringBuilder();
+ destBuilder.append(jingleConnection.getSessionId());
+ if (candidate.isOurs()) {
+ destBuilder.append(jingleConnection.getAccount().getFullJid());
+ destBuilder.append(jingleConnection.getCounterPart());
+ } else {
+ destBuilder.append(jingleConnection.getCounterPart());
+ destBuilder.append(jingleConnection.getAccount().getFullJid());
+ }
+ 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;
+ }
+ int count;
+ byte[] buffer = new byte[8192];
+ while ((count = fileInputStream.read(buffer)) > 0) {
+ outputStream.write(buffer, 0, count);
+ digest.update(buffer, 0, count);
+ }
+ 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;
+ }
+ 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;
+ }
+ }
+ 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.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/eu/siacs/conversations/xmpp/jingle/JingleTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java
new file mode 100644
index 00000000..1374e61c
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java
@@ -0,0 +1,13 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import eu.siacs.conversations.entities.DownloadableFile;
+
+public abstract class JingleTransport {
+ public abstract void connect(final OnTransportConnected callback);
+
+ public abstract void receive(final DownloadableFile file,
+ final OnFileTransmissionStatusChanged callback);
+
+ public abstract void send(final DownloadableFile file,
+ final OnFileTransmissionStatusChanged callback);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java
new file mode 100644
index 00000000..e45e7441
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java
@@ -0,0 +1,9 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import eu.siacs.conversations.entities.DownloadableFile;
+
+public interface OnFileTransmissionStatusChanged {
+ public void onFileTransmitted(DownloadableFile file);
+
+ public void onFileTransferAborted();
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java
new file mode 100644
index 00000000..2aaf62a1
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java
@@ -0,0 +1,9 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xmpp.PacketReceived;
+import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+
+public interface OnJinglePacketReceived extends PacketReceived {
+ public void onJinglePacketReceived(Account account, JinglePacket packet);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java
new file mode 100644
index 00000000..03a437b2
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java
@@ -0,0 +1,6 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+public interface OnPrimaryCandidateFound {
+ public void onPrimaryCandidateFound(boolean success,
+ JingleCandidate canditate);
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java
new file mode 100644
index 00000000..38f03c5d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+public interface OnTransportConnected {
+ public void failed();
+
+ public void established();
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java
new file mode 100644
index 00000000..bcadbe77
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java
@@ -0,0 +1,102 @@
+package eu.siacs.conversations.xmpp.jingle.stanzas;
+
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.xml.Element;
+
+public class Content extends Element {
+
+ private String transportId;
+
+ private Content(String name) {
+ super(name);
+ }
+
+ public Content() {
+ super("content");
+ }
+
+ public Content(String creator, String name) {
+ super("content");
+ this.setAttribute("creator", creator);
+ this.setAttribute("name", name);
+ }
+
+ public void setTransportId(String sid) {
+ this.transportId = sid;
+ }
+
+ public 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/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java
new file mode 100644
index 00000000..77a73643
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java
@@ -0,0 +1,95 @@
+package eu.siacs.conversations.xmpp.jingle.stanzas;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class JinglePacket extends IqPacket {
+ Content content = null;
+ Reason reason = null;
+ Element jingle = new Element("jingle");
+
+ @Override
+ public Element addChild(Element child) {
+ if ("jingle".equals(child.getName())) {
+ Element contentElement = child.findChild("content");
+ if (contentElement != null) {
+ this.content = new Content();
+ this.content.setChildren(contentElement.getChildren());
+ this.content.setAttributes(contentElement.getAttributes());
+ }
+ Element reasonElement = child.findChild("reason");
+ if (reasonElement != null) {
+ this.reason = new Reason();
+ this.reason.setChildren(reasonElement.getChildren());
+ this.reason.setAttributes(reasonElement.getAttributes());
+ }
+ this.jingle.setAttributes(child.getAttributes());
+ }
+ return child;
+ }
+
+ public JinglePacket setContent(Content content) {
+ this.content = content;
+ return this;
+ }
+
+ public Content getJingleContent() {
+ if (this.content == null) {
+ this.content = new Content();
+ }
+ return this.content;
+ }
+
+ public JinglePacket setReason(Reason reason) {
+ this.reason = reason;
+ return this;
+ }
+
+ public Reason getReason() {
+ return this.reason;
+ }
+
+ private void build() {
+ this.children.clear();
+ this.jingle.clearChildren();
+ this.jingle.setAttribute("xmlns", "urn:xmpp:jingle:1");
+ if (this.content != null) {
+ jingle.addChild(this.content);
+ }
+ if (this.reason != null) {
+ jingle.addChild(this.reason);
+ }
+ this.children.add(jingle);
+ this.setAttribute("type", "set");
+ }
+
+ public String getSessionId() {
+ return this.jingle.getAttribute("sid");
+ }
+
+ public void setSessionId(String sid) {
+ this.jingle.setAttribute("sid", sid);
+ }
+
+ @Override
+ public String toString() {
+ this.build();
+ return super.toString();
+ }
+
+ public void setAction(String action) {
+ this.jingle.setAttribute("action", action);
+ }
+
+ public String getAction() {
+ return this.jingle.getAttribute("action");
+ }
+
+ public void setInitiator(String initiator) {
+ this.jingle.setAttribute("initiator", initiator);
+ }
+
+ public boolean isAction(String action) {
+ return action.equalsIgnoreCase(this.getAction());
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java
new file mode 100644
index 00000000..610d5e76
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java
@@ -0,0 +1,13 @@
+package eu.siacs.conversations.xmpp.jingle.stanzas;
+
+import eu.siacs.conversations.xml.Element;
+
+public class Reason extends Element {
+ private Reason(String name) {
+ super(name);
+ }
+
+ public Reason() {
+ super("reason");
+ }
+}