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