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 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 file.getParentFile().mkdirs();
105 file.createNewFile();
106 this.fileOutputStream = connection.getFileOutputStream();
107 if (this.fileOutputStream == null) {
108 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not create output stream");
109 callback.onFileTransferAborted();
110 return;
111 }
112 this.remainingSize = this.fileSize = file.getExpectedSize();
113 } catch (final NoSuchAlgorithmException | IOException e) {
114 Log.d(Config.LOGTAG,account.getJid().toBareJid()+" "+e.getMessage());
115 callback.onFileTransferAborted();
116 }
117 }
118
119 @Override
120 public void send(DownloadableFile file,
121 OnFileTransmissionStatusChanged callback) {
122 this.onFileTransmissionStatusChanged = callback;
123 this.file = file;
124 try {
125 this.remainingSize = this.file.getExpectedSize();
126 this.fileSize = this.remainingSize;
127 this.digest = MessageDigest.getInstance("SHA-1");
128 this.digest.reset();
129 fileInputStream = connection.getFileInputStream();
130 if (fileInputStream == null) {
131 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could no create input stream");
132 callback.onFileTransferAborted();
133 return;
134 }
135 if (this.connected) {
136 this.sendNextBlock();
137 }
138 } catch (NoSuchAlgorithmException e) {
139 callback.onFileTransferAborted();
140 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": "+e.getMessage());
141 }
142 }
143
144 @Override
145 public void disconnect() {
146 this.connected = false;
147 if (this.fileOutputStream != null) {
148 try {
149 this.fileOutputStream.close();
150 } catch (IOException e) {
151
152 }
153 }
154 if (this.fileInputStream != null) {
155 try {
156 this.fileInputStream.close();
157 } catch (IOException e) {
158
159 }
160 }
161 }
162
163 private void sendNextBlock() {
164 byte[] buffer = new byte[this.blockSize];
165 try {
166 int count = fileInputStream.read(buffer);
167 if (count == -1) {
168 sendClose();
169 file.setSha1Sum(digest.digest());
170 this.onFileTransmissionStatusChanged.onFileTransmitted(file);
171 fileInputStream.close();
172 return;
173 } else if (count != buffer.length) {
174 int rem = fileInputStream.read(buffer,count,buffer.length-count);
175 if (rem > 0) {
176 count += rem;
177 }
178 }
179 this.remainingSize -= count;
180 this.digest.update(buffer,0,count);
181 String base64 = Base64.encodeToString(buffer,0,count, Base64.NO_WRAP);
182 IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
183 iq.setTo(this.counterpart);
184 Element data = iq.addChild("data", "http://jabber.org/protocol/ibb");
185 data.setAttribute("seq", Integer.toString(this.seq));
186 data.setAttribute("block-size", Integer.toString(this.blockSize));
187 data.setAttribute("sid", this.sessionId);
188 data.setContent(base64);
189 this.account.getXmppConnection().sendIqPacket(iq, this.onAckReceived);
190 this.account.getXmppConnection().r(); //don't fill up stanza queue too much
191 this.seq++;
192 if (this.remainingSize > 0) {
193 connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
194 } else {
195 sendClose();
196 file.setSha1Sum(digest.digest());
197 this.onFileTransmissionStatusChanged.onFileTransmitted(file);
198 fileInputStream.close();
199 }
200 } catch (IOException e) {
201 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": io exception during sendNextBlock() "+e.getMessage());
202 FileBackend.close(fileInputStream);
203 this.onFileTransmissionStatusChanged.onFileTransferAborted();
204 }
205 }
206
207 private void receiveNextBlock(String data) {
208 try {
209 byte[] buffer = Base64.decode(data, Base64.NO_WRAP);
210 if (this.remainingSize < buffer.length) {
211 buffer = Arrays.copyOfRange(buffer, 0, (int) this.remainingSize);
212 }
213 this.remainingSize -= buffer.length;
214 this.fileOutputStream.write(buffer);
215 this.digest.update(buffer);
216 if (this.remainingSize <= 0) {
217 file.setSha1Sum(digest.digest());
218 fileOutputStream.flush();
219 fileOutputStream.close();
220 this.onFileTransmissionStatusChanged.onFileTransmitted(file);
221 } else {
222 connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
223 }
224 } catch (Exception e) {
225 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": "+e.getMessage());
226 FileBackend.close(fileOutputStream);
227 this.onFileTransmissionStatusChanged.onFileTransferAborted();
228 }
229 }
230
231 public void deliverPayload(IqPacket packet, Element payload) {
232 if (payload.getName().equals("open")) {
233 if (!established) {
234 established = true;
235 connected = true;
236 this.receiveNextBlock("");
237 this.account.getXmppConnection().sendIqPacket(
238 packet.generateResponse(IqPacket.TYPE.RESULT), null);
239 } else {
240 this.account.getXmppConnection().sendIqPacket(
241 packet.generateResponse(IqPacket.TYPE.ERROR), null);
242 }
243 } else if (connected && payload.getName().equals("data")) {
244 this.receiveNextBlock(payload.getContent());
245 this.account.getXmppConnection().sendIqPacket(
246 packet.generateResponse(IqPacket.TYPE.RESULT), null);
247 } else if (connected && payload.getName().equals("close")) {
248 this.connected = false;
249 this.account.getXmppConnection().sendIqPacket(
250 packet.generateResponse(IqPacket.TYPE.RESULT), null);
251 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": received ibb close");
252 } else {
253 Log.d(Config.LOGTAG,payload.toString());
254 // TODO some sort of exception
255 }
256 }
257}