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