added support for ibb

Daniel Gultsch created

Change summary

src/eu/siacs/conversations/services/XmppConnectionService.java      |   2 
src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java        | 113 
src/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java |  27 
src/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java   | 183 
src/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java   |   6 
src/eu/siacs/conversations/xmpp/jingle/JingleTransport.java         |   3 
src/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java    |   2 
7 files changed, 313 insertions(+), 23 deletions(-)

Detailed changes

src/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -368,6 +368,8 @@ public class XmppConnectionService extends Service {
 					processRosterItems(account, query);
 					mergePhoneContactsWithRoster(null);
 				}
+			} else if (packet.hasChild("open","http://jabber.org/protocol/ibb")||packet.hasChild("data","http://jabber.org/protocol/ibb")) {
+				XmppConnectionService.this.mJingleConnectionManager.deliverIbbPacket(account,packet);
 			} else {
 				Log.d(LOGTAG,"iq packet arrived "+packet.toString());
 			}

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

@@ -32,6 +32,8 @@ public class JingleConnection {
 	public static final int STATUS_TRANSMITTING = 5;
 	public static final int STATUS_FAILED = 99;
 	
+	private int ibbBlockSize = 4096;
+	
 	private int status = -1;
 	private Message message;
 	private String sessionId;
@@ -39,7 +41,7 @@ public class JingleConnection {
 	private String initiator;
 	private String responder;
 	private List<JingleCandidate> candidates = new ArrayList<JingleCandidate>();
-	private HashMap<String, SocksConnection> connections = new HashMap<String, SocksConnection>();
+	private HashMap<String, JingleSocks5Transport> connections = new HashMap<String, JingleSocks5Transport>();
 	
 	private String transportId;
 	private Element fileOffer;
@@ -126,7 +128,15 @@ public class JingleConnection {
 			} else if (packet.isAction("session-accept")) {
 			accept(packet);
 		} else if (packet.isAction("transport-info")) {
-			transportInfo(packet);
+			receiveTransportInfo(packet);
+		} else if (packet.isAction("transport-replace")) {
+			if (packet.getJingleContent().hasIbbTransport()) {
+				this.receiveFallbackToIbb(packet);
+			} else {
+				Log.d("xmppService","trying to fallback to something unknown"+packet.toString());
+			}
+		} else if (packet.isAction("transport-accept")) {
+			this.receiveTransportAccept(packet);
 		} else {
 			Log.d("xmppService","packet arrived in connection. action was "+packet.getAction());
 		}
@@ -146,9 +156,9 @@ public class JingleConnection {
 				@Override
 				public void onPrimaryCandidateFound(boolean success, final JingleCandidate candidate) {
 					if (success) {
-						final SocksConnection socksConnection = new SocksConnection(JingleConnection.this, candidate);
+						final JingleSocks5Transport socksConnection = new JingleSocks5Transport(JingleConnection.this, candidate);
 						connections.put(candidate.getCid(), socksConnection);
-						socksConnection.connect(new OnSocksConnection() {
+						socksConnection.connect(new OnTransportConnected() {
 							
 							@Override
 							public void failed() {
@@ -248,9 +258,9 @@ public class JingleConnection {
 				content.setFileOffer(fileOffer);
 				content.setTransportId(transportId);
 				if ((success)&&(!equalCandidateExists(candidate))) {
-					final SocksConnection socksConnection = new SocksConnection(JingleConnection.this, candidate);
+					final JingleSocks5Transport socksConnection = new JingleSocks5Transport(JingleConnection.this, candidate);
 					connections.put(candidate.getCid(), socksConnection);
-					socksConnection.connect(new OnSocksConnection() {
+					socksConnection.connect(new OnTransportConnected() {
 						
 						@Override
 						public void failed() {
@@ -307,7 +317,7 @@ public class JingleConnection {
 		account.getXmppConnection().sendIqPacket(response, null);
 	}
 
-	private void transportInfo(JinglePacket packet) {
+	private void receiveTransportInfo(JinglePacket packet) {
 		Content content = packet.getJingleContent();
 		if (content.hasSocks5Transport()) {
 			if (content.socks5transport().hasChild("activated")) {
@@ -345,13 +355,14 @@ public class JingleConnection {
 	}
 
 	private void connect() {
-		final SocksConnection connection = chooseConnection();
+		final JingleSocks5Transport connection = chooseConnection();
 		this.transport = connection;
 		if (connection==null) {
 			Log.d("xmppService","could not find suitable candidate");
 			this.disconnect();
-			this.status = STATUS_FAILED;
-			this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND_FAILED);
+			if (this.initiator.equals(account.getFullJid())) {
+				this.sendFallbackToIbb();
+			}
 		} else {
 			this.status = STATUS_TRANSMITTING;
 			if (connection.isProxy()) {
@@ -386,12 +397,12 @@ public class JingleConnection {
 		}
 	}
 	
-	private SocksConnection chooseConnection() {
-		SocksConnection connection = null;
-		Iterator<Entry<String, SocksConnection>> it = this.connections.entrySet().iterator();
+	private JingleSocks5Transport chooseConnection() {
+		JingleSocks5Transport connection = null;
+		Iterator<Entry<String, JingleSocks5Transport>> it = this.connections.entrySet().iterator();
 	    while (it.hasNext()) {
-	    	Entry<String, SocksConnection> pairs = it.next();
-	    	SocksConnection currentConnection = pairs.getValue();
+	    	Entry<String, JingleSocks5Transport> pairs = it.next();
+	    	JingleSocks5Transport currentConnection = pairs.getValue();
 	    	//Log.d("xmppService","comparing candidate: "+currentConnection.getCandidate().toString());
 	        if (currentConnection.isEstablished()&&(currentConnection.getCandidate().isUsedByCounterpart()||(!currentConnection.getCandidate().isOurs()))) {
 	        	//Log.d("xmppService","is usable");
@@ -430,6 +441,62 @@ public class JingleConnection {
 		this.mXmppConnectionService.markMessage(this.message, Message.STATUS_RECIEVED);
 	}
 	
+	private void sendFallbackToIbb() {
+		JinglePacket packet = this.bootstrapPacket("transport-replace");
+		Content content = new Content("initiator","a-file-offer");
+		this.transportId = this.mJingleConnectionManager.nextRandomId();
+		content.setTransportId(this.transportId);
+		content.ibbTransport().setAttribute("block-size",""+this.ibbBlockSize);
+		packet.setContent(content);
+		this.sendJinglePacket(packet);
+	}
+	
+	private void receiveFallbackToIbb(JinglePacket packet) {
+		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, onFileTransmitted);
+		JinglePacket answer = bootstrapPacket("transport-accept");
+		Content content = new Content("initiator", "a-file-offer");
+		content.setTransportId(this.transportId);
+		content.ibbTransport().setAttribute("block-size", ""+this.ibbBlockSize);
+		answer.setContent(content);
+		this.sendJinglePacket(answer);
+	}
+	
+	private void 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("xmppService","ibb open failed");
+				}
+				
+				@Override
+				public void established() {
+					JingleConnection.this.transport.send(file, onFileTransmitted);
+				}
+			});
+		} else {
+			Log.d("xmppService","invalid transport accept");
+		}
+	}
+	
 	private void finish() {
 		this.status = STATUS_FINISHED;
 		this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND);
@@ -453,9 +520,9 @@ public class JingleConnection {
 	}
 	
 	private void connectWithCandidate(final JingleCandidate candidate) {
-		final SocksConnection socksConnection = new SocksConnection(this,candidate);
+		final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this,candidate);
 		connections.put(candidate.getCid(), socksConnection);
-		socksConnection.connect(new OnSocksConnection() {
+		socksConnection.connect(new OnTransportConnected() {
 			
 			@Override
 			public void failed() {
@@ -472,9 +539,9 @@ public class JingleConnection {
 	}
 
 	private void disconnect() {
-		Iterator<Entry<String, SocksConnection>> it = this.connections.entrySet().iterator();
+		Iterator<Entry<String, JingleSocks5Transport>> it = this.connections.entrySet().iterator();
 	    while (it.hasNext()) {
-	        Entry<String, SocksConnection> pairs = it.next();
+	        Entry<String, JingleSocks5Transport> pairs = it.next();
 	        pairs.getValue().disconnect();
 	        it.remove();
 	    }
@@ -564,4 +631,12 @@ public class JingleConnection {
 		public void success();
 		public void failed();
 	}
+
+	public boolean hasTransportId(String sid) {
+		return sid.equals(this.transportId);
+	}
+	
+	public JingleTransport getTransport() {
+		return this.transport;
+	}
 }

src/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java 🔗

@@ -124,4 +124,31 @@ public class JingleConnectionManager {
 			return 524288;
 		}
 	}
+
+	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.hasTransportId(sid)) {
+					JingleTransport transport = connection.getTransport();
+					if (transport instanceof JingleInbandTransport) {
+						JingleInbandTransport inbandTransport = (JingleInbandTransport) transport;
+						inbandTransport.deliverPayload(packet,payload);
+						return;
+					}
+				}
+			}
+			Log.d("xmppService","couldnt deliver payload: "+payload.toString());
+		} else {
+			Log.d("xmppService","no sid found in incomming ibb packet");
+		}
+	}
 }

src/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java 🔗

@@ -0,0 +1,183 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import android.util.Base64;
+import android.util.Log;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.PacketReceived;
+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 JingleFile file;
+	
+	private FileInputStream fileInputStream = null;
+	private FileOutputStream fileOutputStream;
+	private long remainingSize;
+	private MessageDigest digest;
+
+	private OnFileTransmitted onFileTransmitted;
+	
+	private OnIqPacketReceived onAckReceived = new OnIqPacketReceived() {
+		@Override
+		public void onIqPacketReceived(Account account, IqPacket packet) {
+			Log.d("xmppService", "on ack received");
+			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", "" + 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(JingleFile file, OnFileTransmitted callback) {
+		this.onFileTransmitted = callback;
+		this.file = file;
+		Log.d("xmppService", "receiving file over ibb");
+		try {
+			this.digest = MessageDigest.getInstance("SHA-1");
+			digest.reset();
+			file.getParentFile().mkdirs();
+			file.createNewFile();
+			this.fileOutputStream = new FileOutputStream(file);
+			this.remainingSize = file.getExpectedSize();
+		} catch (NoSuchAlgorithmException e) {
+			e.printStackTrace();
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+	}
+
+	@Override
+	public void send(JingleFile file, OnFileTransmitted callback) {
+		this.onFileTransmitted = callback;
+		this.file = file;
+		Log.d("xmppService", "sending file over ibb");
+		try {
+			this.digest = MessageDigest.getInstance("SHA-1");
+			this.digest.reset();
+			fileInputStream = new FileInputStream(file);
+			this.sendNextBlock();
+		} catch (FileNotFoundException e) {
+			e.printStackTrace();
+		} catch (NoSuchAlgorithmException e) {
+			e.printStackTrace();
+		}
+	}
+
+	private void sendNextBlock() {
+		byte[] buffer = new byte[this.bufferSize];
+		try {
+			int count = fileInputStream.read(buffer);
+			if (count==-1) {
+				file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
+				this.onFileTransmitted.onFileTransmitted(file);
+			} else {
+				this.digest.update(buffer);
+				String base64 = Base64.encodeToString(buffer, Base64.DEFAULT);
+				IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
+				iq.setTo(this.counterpart);
+				Element data = iq
+						.addChild("data", "http://jabber.org/protocol/ibb");
+				data.setAttribute("seq", "" + this.seq);
+				data.setAttribute("block-size", "" + this.blockSize);
+				data.setAttribute("sid", this.sessionId);
+				data.setContent(base64);
+				this.account.getXmppConnection().sendIqPacket(iq,
+						this.onAckReceived);
+				this.seq++;
+			}
+		} catch (IOException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		}
+	}
+
+	private void receiveNextBlock(String data) {
+		try {
+			byte[] buffer = Base64.decode(data, Base64.DEFAULT);
+			this.remainingSize -= buffer.length;
+
+			this.fileOutputStream.write(buffer);
+
+			this.digest.update(buffer);
+			Log.d("xmppService", "remaining file size:" + this.remainingSize);
+			if (this.remainingSize <= 0) {
+				file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
+				Log.d("xmppService","file name: "+file.getAbsolutePath());
+				fileOutputStream.flush();
+				this.onFileTransmitted.onFileTransmitted(file);
+			}
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+	}
+
+	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 {
+			Log.d("xmppServic","couldnt deliver payload "+packet.toString());
+		}
+	}
+}

src/eu/siacs/conversations/xmpp/jingle/SocksConnection.java → src/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java 🔗

@@ -18,7 +18,7 @@ import eu.siacs.conversations.xml.Element;
 import android.util.Log;
 import android.widget.Button;
 
-public class SocksConnection extends JingleTransport {
+public class JingleSocks5Transport extends JingleTransport {
 	private JingleCandidate candidate;
 	private String destination;
 	private OutputStream outputStream;
@@ -26,7 +26,7 @@ public class SocksConnection extends JingleTransport {
 	private boolean isEstablished = false;
 	protected Socket socket;
 
-	public SocksConnection(JingleConnection jingleConnection, JingleCandidate candidate) {
+	public JingleSocks5Transport(JingleConnection jingleConnection, JingleCandidate candidate) {
 		this.candidate = candidate;
 		try {
 			MessageDigest mDigest = MessageDigest.getInstance("SHA-1");
@@ -47,7 +47,7 @@ public class SocksConnection extends JingleTransport {
 		}
 	}
 
-	public void connect(final OnSocksConnection callback) {
+	public void connect(final OnTransportConnected callback) {
 		new Thread(new Runnable() {
 			
 			@Override

src/eu/siacs/conversations/xmpp/jingle/JingleTransport.java 🔗

@@ -1,6 +1,9 @@
 package eu.siacs.conversations.xmpp.jingle;
 
+import eu.siacs.conversations.xml.Element;
+
 public abstract class JingleTransport {
+	public abstract void connect(final OnTransportConnected callback);
 	public abstract void receive(final JingleFile file, final OnFileTransmitted callback);
 	public abstract void send(final JingleFile file, final OnFileTransmitted callback);
 }