support jingle ft:4 to be compatible with swift

Daniel Gultsch created

Conversations and Gajim both have an implementation bug that sends the jingle session id instead of the transport id (compare XEP-260 2.2). This commit has a work around for this that remains buggy when using ft:3. If gajim is ever to fix this we will be incompatbile. gajim should implement ft:4 instead. (gajim to gajim is broken as well)

Change summary

src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java       |  4 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java      | 55 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java |  8 
src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java       | 70 
4 files changed, 105 insertions(+), 32 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java 🔗

@@ -16,11 +16,13 @@ import eu.siacs.conversations.Config;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.PhoneHelper;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
 
 public abstract class AbstractGenerator {
 	private final String[] FEATURES = {
 			"urn:xmpp:jingle:1",
-			"urn:xmpp:jingle:apps:file-transfer:3",
+			Content.Version.FT_3.getNamespace(),
+			Content.Version.FT_4.getNamespace(),
 			"urn:xmpp:jingle:transports:s5b:1",
 			"urn:xmpp:jingle:transports:ibb:1",
 			"http://jabber.org/protocol/muc",

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java 🔗

@@ -21,6 +21,7 @@ import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.DownloadableFile;
 import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Presence;
 import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.entities.TransferablePlaceholder;
 import eu.siacs.conversations.persistance.FileBackend;
@@ -45,6 +46,8 @@ public class JingleConnection implements Transferable {
 	protected static final int JINGLE_STATUS_TRANSMITTING = 5;
 	protected static final int JINGLE_STATUS_FAILED = 99;
 
+	private Content.Version ftVersion = Content.Version.FT_3;
+
 	private int ibbBlockSize = 8192;
 
 	private int mJingleStatus = -1;
@@ -238,12 +241,14 @@ public class JingleConnection implements Transferable {
 		this.contentCreator = "initiator";
 		this.contentName = this.mJingleConnectionManager.nextRandomId();
 		this.message = message;
+		this.account = message.getConversation().getAccount();
+		upgradeNamespace();
 		this.message.setTransferable(this);
 		this.mStatus = Transferable.STATUS_UPLOADING;
-		this.account = message.getConversation().getAccount();
 		this.initiator = this.account.getJid();
 		this.responder = this.message.getCounterpart();
 		this.sessionId = this.mJingleConnectionManager.nextRandomId();
+		this.transportId = this.mJingleConnectionManager.nextRandomId();
 		if (this.candidates.size() > 0) {
 			this.sendInitRequest();
 		} else {
@@ -287,6 +292,20 @@ public class JingleConnection implements Transferable {
 
 	}
 
+	private void upgradeNamespace() {
+		Jid jid = this.message.getCounterpart();
+		String resource = jid != null ?jid.getResourcepart() : null;
+		if (resource != null) {
+			Presence presence = this.account.getRoster().getContact(jid).getPresences().getPresences().get(resource);
+			if (presence != null) {
+				List<String> features = presence.getServiceDiscoveryResult().getFeatures();
+				if (features.contains(Content.Version.FT_4.getNamespace())) {
+					this.ftVersion = Content.Version.FT_4;
+				}
+			}
+		}
+	}
+
 	public void init(Account account, JinglePacket packet) {
 		this.mJingleStatus = JINGLE_STATUS_INITIATED;
 		Conversation conversation = this.mXmppConnectionService
@@ -307,7 +326,13 @@ public class JingleConnection implements Transferable {
 		this.contentName = content.getAttribute("name");
 		this.transportId = content.getTransportId();
 		this.mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren()));
-		this.fileOffer = packet.getJingleContent().getFileOffer();
+		this.ftVersion = content.getVersion();
+		if (ftVersion == null) {
+			this.sendCancel();
+			this.fail();
+			return;
+		}
+		this.fileOffer = content.getFileOffer(this.ftVersion);
 
 		mXmppConnectionService.sendIqPacket(account,packet.generateResponse(IqPacket.TYPE.RESULT),null);
 
@@ -431,24 +456,23 @@ public class JingleConnection implements Transferable {
 					this.file.setKeyAndIv(conversation.getSymmetricKey());
 					pair = AbstractConnectionManager.createInputStream(this.file, false);
 					this.file.setExpectedSize(pair.second);
-					content.setFileOffer(this.file, true);
+					content.setFileOffer(this.file, true, this.ftVersion);
 				} else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
 					this.file.setKey(mXmppAxolotlMessage.getInnerKey());
 					this.file.setIv(mXmppAxolotlMessage.getIV());
 					pair = AbstractConnectionManager.createInputStream(this.file, true);
 					this.file.setExpectedSize(pair.second);
-					content.setFileOffer(this.file, false).addChild(mXmppAxolotlMessage.toElement());
+					content.setFileOffer(this.file, false, this.ftVersion).addChild(mXmppAxolotlMessage.toElement());
 				} else {
 					pair = AbstractConnectionManager.createInputStream(this.file, false);
 					this.file.setExpectedSize(pair.second);
-					content.setFileOffer(this.file, false);
+					content.setFileOffer(this.file, false, this.ftVersion);
 				}
 			} catch (FileNotFoundException e) {
 				cancel();
 				return;
 			}
 			this.mFileInputStream = pair.first;
-			this.transportId = this.mJingleConnectionManager.nextRandomId();
 			content.setTransportId(this.transportId);
 			content.socks5transport().setChildren(getCandidatesAsElements());
 			packet.setContent(content);
@@ -488,7 +512,7 @@ public class JingleConnection implements Transferable {
 			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.setFileOffer(fileOffer, ftVersion);
 				content.setTransportId(transportId);
 				if (success && candidate != null && !equalCandidateExists(candidate)) {
 					final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
@@ -626,13 +650,20 @@ public class JingleConnection implements Transferable {
 			this.mJingleStatus = JINGLE_STATUS_TRANSMITTING;
 			if (connection.needsActivation()) {
 				if (connection.getCandidate().isOurs()) {
+					final String sid;
+					if (ftVersion == Content.Version.FT_3) {
+						Log.d(Config.LOGTAG,account.getJid().toBareJid()+": use session ID instead of transport ID to activate proxy");
+						sid = getSessionId();
+					} else {
+						sid = getTransportId();
+					}
 					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());
+							.setAttribute("sid", sid);
 					activation.query().addChild("activate")
 							.setContent(this.getCounterPart().toString());
 					mXmppConnectionService.sendIqPacket(account,activation,
@@ -975,6 +1006,14 @@ public class JingleConnection implements Transferable {
 		mXmppConnectionService.updateConversationUi();
 	}
 
+	public String getTransportId() {
+		return this.transportId;
+	}
+
+	public Content.Version getFtVersion() {
+		return this.ftVersion;
+	}
+
 	interface OnProxyActivated {
 		public void success();
 

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java 🔗

@@ -22,6 +22,7 @@ import eu.siacs.conversations.entities.DownloadableFile;
 import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.SocksSocketFactory;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
 
 public class JingleSocks5Transport extends JingleTransport {
 	private JingleCandidate candidate;
@@ -40,7 +41,12 @@ public class JingleSocks5Transport extends JingleTransport {
 		try {
 			MessageDigest mDigest = MessageDigest.getInstance("SHA-1");
 			StringBuilder destBuilder = new StringBuilder();
-			destBuilder.append(jingleConnection.getSessionId());
+			if (jingleConnection.getFtVersion() == Content.Version.FT_3) {
+				Log.d(Config.LOGTAG,this.connection.getAccount().getJid().toBareJid()+": using session Id instead of transport Id for proxy destination");
+				destBuilder.append(jingleConnection.getSessionId());
+			} else {
+				destBuilder.append(jingleConnection.getTransportId());
+			}
 			if (candidate.isOurs()) {
 				destBuilder.append(jingleConnection.getAccount().getJid());
 				destBuilder.append(jingleConnection.getCounterPart());

src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java 🔗

@@ -5,12 +5,23 @@ import eu.siacs.conversations.xml.Element;
 
 public class Content extends Element {
 
-	private String transportId;
+	public enum Version {
+		FT_3("urn:xmpp:jingle:apps:file-transfer:3"),
+		FT_4("urn:xmpp:jingle:apps:file-transfer:4");
+
+		private final String namespace;
+
+		Version(String namespace) {
+			this.namespace = namespace;
+		}
 
-	private Content(String name) {
-		super(name);
+		public String getNamespace() {
+			return namespace;
+		}
 	}
 
+	private String transportId;
+
 	public Content() {
 		super("content");
 	}
@@ -21,15 +32,28 @@ public class Content extends Element {
 		this.setAttribute("name", name);
 	}
 
+	public Version getVersion() {
+		if (hasChild("description", Version.FT_3.namespace)) {
+			return Version.FT_3;
+		} else if (hasChild("description" , Version.FT_4.namespace)) {
+			return Version.FT_4;
+		}
+		return null;
+	}
+
 	public void setTransportId(String sid) {
 		this.transportId = sid;
 	}
 
-	public Element setFileOffer(DownloadableFile actualFile, boolean otr) {
-		Element description = this.addChild("description",
-				"urn:xmpp:jingle:apps:file-transfer:3");
-		Element offer = description.addChild("offer");
-		Element file = offer.addChild("file");
+	public Element setFileOffer(DownloadableFile actualFile, boolean otr, Version version) {
+		Element description = this.addChild("description", version.namespace);
+		Element file;
+		if (version == Version.FT_3) {
+			Element offer = description.addChild("offer");
+			file = offer.addChild("file");
+		} else {
+			file = description.addChild("file");
+		}
 		file.addChild("size").setContent(Long.toString(actualFile.getExpectedSize()));
 		if (otr) {
 			file.addChild("name").setContent(actualFile.getName() + ".otr");
@@ -39,27 +63,29 @@ public class Content extends Element {
 		return file;
 	}
 
-	public Element getFileOffer() {
-		Element description = this.findChild("description",
-				"urn:xmpp:jingle:apps:file-transfer:3");
+	public Element getFileOffer(Version version) {
+		Element description = this.findChild("description", version.namespace);
 		if (description == null) {
 			return null;
 		}
-		Element offer = description.findChild("offer");
-		if (offer == null) {
-			return null;
+		if (version == Version.FT_3) {
+			Element offer = description.findChild("offer");
+			if (offer == null) {
+				return null;
+			}
+			return offer.findChild("file");
+		} else {
+			return description.findChild("file");
 		}
-		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");
+	public void setFileOffer(Element fileOffer, Version version) {
+		Element description = this.addChild("description", version.namespace);
+		if (version == Version.FT_3) {
+			description.addChild("offer").addChild(fileOffer);
+		} else {
+			description.addChild(fileOffer);
 		}
-		description.addChild(fileOffer);
 	}
 
 	public String getTransportId() {