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