JingleInbandTransport.java

  1package eu.siacs.conversations.xmpp.jingle;
  2
  3import android.util.Base64;
  4import android.util.Log;
  5
  6import java.io.IOException;
  7import java.io.InputStream;
  8import java.io.OutputStream;
  9import java.security.MessageDigest;
 10import java.security.NoSuchAlgorithmException;
 11import java.util.Arrays;
 12
 13import eu.siacs.conversations.Config;
 14import eu.siacs.conversations.entities.Account;
 15import eu.siacs.conversations.entities.DownloadableFile;
 16import eu.siacs.conversations.persistance.FileBackend;
 17import eu.siacs.conversations.services.AbstractConnectionManager;
 18import eu.siacs.conversations.utils.CryptoHelper;
 19import eu.siacs.conversations.xml.Element;
 20import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 21import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 22import rocks.xmpp.addr.Jid;
 23
 24public class JingleInbandTransport extends JingleTransport {
 25
 26    private Account account;
 27    private Jid counterpart;
 28    private int blockSize;
 29    private int seq = 0;
 30    private String sessionId;
 31
 32    private boolean established = false;
 33
 34    private boolean connected = true;
 35
 36    private DownloadableFile file;
 37    private JingleConnection connection;
 38
 39    private InputStream fileInputStream = null;
 40    private InputStream innerInputStream = null;
 41    private OutputStream fileOutputStream = null;
 42    private long remainingSize = 0;
 43    private long fileSize = 0;
 44    private MessageDigest digest;
 45
 46    private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged;
 47
 48    private OnIqPacketReceived onAckReceived = new OnIqPacketReceived() {
 49        @Override
 50        public void onIqPacketReceived(Account account, IqPacket packet) {
 51            if (connected && packet.getType() == IqPacket.TYPE.RESULT) {
 52                if (remainingSize > 0) {
 53                    sendNextBlock();
 54                }
 55            }
 56        }
 57    };
 58
 59    public JingleInbandTransport(final JingleConnection connection, final String sid, final int blocksize) {
 60        this.connection = connection;
 61        this.account = connection.getAccount();
 62        this.counterpart = connection.getCounterPart();
 63        this.blockSize = blocksize;
 64        this.sessionId = sid;
 65    }
 66
 67    private void sendClose() {
 68        IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
 69        iq.setTo(this.counterpart);
 70        Element close = iq.addChild("close", "http://jabber.org/protocol/ibb");
 71        close.setAttribute("sid", this.sessionId);
 72        this.account.getXmppConnection().sendIqPacket(iq, null);
 73    }
 74
 75    public void connect(final OnTransportConnected callback) {
 76        IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
 77        iq.setTo(this.counterpart);
 78        Element open = iq.addChild("open", "http://jabber.org/protocol/ibb");
 79        open.setAttribute("sid", this.sessionId);
 80        open.setAttribute("stanza", "iq");
 81        open.setAttribute("block-size", Integer.toString(this.blockSize));
 82        this.connected = true;
 83        this.account.getXmppConnection().sendIqPacket(iq, (account, packet) -> {
 84            if (packet.getType() != IqPacket.TYPE.RESULT) {
 85                callback.failed();
 86            } else {
 87                callback.established();
 88            }
 89        });
 90    }
 91
 92    @Override
 93    public void receive(DownloadableFile file, OnFileTransmissionStatusChanged callback) {
 94        this.onFileTransmissionStatusChanged = callback;
 95        this.file = file;
 96        try {
 97            this.digest = MessageDigest.getInstance("SHA-1");
 98            digest.reset();
 99            this.fileOutputStream = connection.getFileOutputStream();
100            if (this.fileOutputStream == null) {
101                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not create output stream");
102                callback.onFileTransferAborted();
103                return;
104            }
105            this.remainingSize = this.fileSize = file.getExpectedSize();
106        } catch (final NoSuchAlgorithmException | IOException e) {
107            Log.d(Config.LOGTAG, account.getJid().asBareJid() + " " + e.getMessage());
108            callback.onFileTransferAborted();
109        }
110    }
111
112    @Override
113    public void send(DownloadableFile file, OnFileTransmissionStatusChanged callback) {
114        this.onFileTransmissionStatusChanged = callback;
115        this.file = file;
116        try {
117            this.remainingSize = this.file.getExpectedSize();
118            this.fileSize = this.remainingSize;
119            this.digest = MessageDigest.getInstance("SHA-1");
120            this.digest.reset();
121            fileInputStream = connection.getFileInputStream();
122            if (fileInputStream == null) {
123                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could no create input stream");
124                callback.onFileTransferAborted();
125                return;
126            }
127            innerInputStream = AbstractConnectionManager.upgrade(file, fileInputStream);
128            if (this.connected) {
129                this.sendNextBlock();
130            }
131        } catch (Exception e) {
132            callback.onFileTransferAborted();
133            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage());
134        }
135    }
136
137    @Override
138    public void disconnect() {
139        this.connected = false;
140        FileBackend.close(fileOutputStream);
141        FileBackend.close(fileInputStream);
142    }
143
144    private void sendNextBlock() {
145        byte[] buffer = new byte[this.blockSize];
146        try {
147            int count = innerInputStream.read(buffer);
148            if (count == -1) {
149                sendClose();
150                file.setSha1Sum(digest.digest());
151                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sendNextBlock() count was -1");
152                this.onFileTransmissionStatusChanged.onFileTransmitted(file);
153                fileInputStream.close();
154                return;
155            } else if (count != buffer.length) {
156                int rem = innerInputStream.read(buffer, count, buffer.length - count);
157                if (rem > 0) {
158                    count += rem;
159                }
160            }
161            this.remainingSize -= count;
162            this.digest.update(buffer, 0, count);
163            String base64 = Base64.encodeToString(buffer, 0, count, Base64.NO_WRAP);
164            IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
165            iq.setTo(this.counterpart);
166            Element data = iq.addChild("data", "http://jabber.org/protocol/ibb");
167            data.setAttribute("seq", Integer.toString(this.seq));
168            data.setAttribute("block-size", Integer.toString(this.blockSize));
169            data.setAttribute("sid", this.sessionId);
170            data.setContent(base64);
171            this.account.getXmppConnection().sendIqPacket(iq, this.onAckReceived);
172            this.account.getXmppConnection().r(); //don't fill up stanza queue too much
173            this.seq++;
174            connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
175            if (this.remainingSize <= 0) {
176                sendClose();
177                file.setSha1Sum(digest.digest());
178                this.onFileTransmissionStatusChanged.onFileTransmitted(file);
179                fileInputStream.close();
180            }
181        } catch (IOException e) {
182            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": io exception during sendNextBlock() " + e.getMessage());
183            FileBackend.close(fileInputStream);
184            this.onFileTransmissionStatusChanged.onFileTransferAborted();
185        }
186    }
187
188    private void receiveNextBlock(String data) {
189        try {
190            byte[] buffer = Base64.decode(data, Base64.NO_WRAP);
191            if (this.remainingSize < buffer.length) {
192                buffer = Arrays.copyOfRange(buffer, 0, (int) this.remainingSize);
193            }
194            this.remainingSize -= buffer.length;
195            this.fileOutputStream.write(buffer);
196            this.digest.update(buffer);
197            if (this.remainingSize <= 0) {
198                file.setSha1Sum(digest.digest());
199                fileOutputStream.flush();
200                fileOutputStream.close();
201                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": receive next block nothing remaining");
202                this.onFileTransmissionStatusChanged.onFileTransmitted(file);
203            } else {
204                connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
205            }
206        } catch (Exception e) {
207            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage());
208            FileBackend.close(fileOutputStream);
209            this.onFileTransmissionStatusChanged.onFileTransferAborted();
210        }
211    }
212
213    public void deliverPayload(IqPacket packet, Element payload) {
214        if (payload.getName().equals("open")) {
215            if (!established) {
216                established = true;
217                connected = true;
218                this.receiveNextBlock("");
219                this.account.getXmppConnection().sendIqPacket(
220                        packet.generateResponse(IqPacket.TYPE.RESULT), null);
221            } else {
222                this.account.getXmppConnection().sendIqPacket(
223                        packet.generateResponse(IqPacket.TYPE.ERROR), null);
224            }
225        } else if (connected && payload.getName().equals("data")) {
226            this.receiveNextBlock(payload.getContent());
227            this.account.getXmppConnection().sendIqPacket(
228                    packet.generateResponse(IqPacket.TYPE.RESULT), null);
229        } else if (connected && payload.getName().equals("close")) {
230            this.connected = false;
231            this.account.getXmppConnection().sendIqPacket(
232                    packet.generateResponse(IqPacket.TYPE.RESULT), null);
233            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received ibb close");
234        } else {
235            Log.d(Config.LOGTAG, payload.toString());
236            // TODO some sort of exception
237        }
238    }
239}