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 callback.onFileTransferAborted();
101 return;
102 }
103 this.remainingSize = this.fileSize = file.getExpectedSize();
104 } catch (final NoSuchAlgorithmException | IOException e) {
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 callback.onFileTransferAborted();
126 return;
127 }
128 if (this.connected) {
129 this.sendNextBlock();
130 }
131 } catch (NoSuchAlgorithmException e) {
132 callback.onFileTransferAborted();
133 }
134 }
135
136 @Override
137 public void disconnect() {
138 this.connected = false;
139 if (this.fileOutputStream != null) {
140 try {
141 this.fileOutputStream.close();
142 } catch (IOException e) {
143
144 }
145 }
146 if (this.fileInputStream != null) {
147 try {
148 this.fileInputStream.close();
149 } catch (IOException e) {
150
151 }
152 }
153 }
154
155 private void sendNextBlock() {
156 byte[] buffer = new byte[this.bufferSize];
157 try {
158 int count = fileInputStream.read(buffer);
159 this.remainingSize -= count;
160 if (count != buffer.length && count != -1) {
161 int rem = fileInputStream.read(buffer,count,buffer.length-count);
162 if (rem > 0) {
163 count += rem;
164 }
165 }
166 this.digest.update(buffer,0,count);
167 String base64 = Base64.encodeToString(buffer,0,count, Base64.NO_WRAP);
168 IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
169 iq.setTo(this.counterpart);
170 Element data = iq.addChild("data", "http://jabber.org/protocol/ibb");
171 data.setAttribute("seq", Integer.toString(this.seq));
172 data.setAttribute("block-size", Integer.toString(this.blockSize));
173 data.setAttribute("sid", this.sessionId);
174 data.setContent(base64);
175 this.account.getXmppConnection().sendIqPacket(iq, this.onAckReceived);
176 this.seq++;
177 if (this.remainingSize > 0) {
178 connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
179 } else {
180 file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
181 this.onFileTransmissionStatusChanged.onFileTransmitted(file);
182 fileInputStream.close();
183 }
184 } catch (IOException e) {
185 Log.d(Config.LOGTAG,e.getMessage());
186 FileBackend.close(fileInputStream);
187 this.onFileTransmissionStatusChanged.onFileTransferAborted();
188 }
189 }
190
191 private void receiveNextBlock(String data) {
192 try {
193 byte[] buffer = Base64.decode(data, Base64.NO_WRAP);
194 if (this.remainingSize < buffer.length) {
195 buffer = Arrays.copyOfRange(buffer, 0, (int) this.remainingSize);
196 }
197 this.remainingSize -= buffer.length;
198 this.fileOutputStream.write(buffer);
199 this.digest.update(buffer);
200 if (this.remainingSize <= 0) {
201 file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
202 fileOutputStream.flush();
203 fileOutputStream.close();
204 this.onFileTransmissionStatusChanged.onFileTransmitted(file);
205 } else {
206 connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
207 }
208 } catch (IOException e) {
209 Log.d(Config.LOGTAG,e.getMessage());
210 FileBackend.close(fileOutputStream);
211 this.onFileTransmissionStatusChanged.onFileTransferAborted();
212 }
213 }
214
215 public void deliverPayload(IqPacket packet, Element payload) {
216 if (payload.getName().equals("open")) {
217 if (!established) {
218 established = true;
219 connected = true;
220 this.receiveNextBlock("");
221 this.account.getXmppConnection().sendIqPacket(
222 packet.generateResponse(IqPacket.TYPE.RESULT), null);
223 } else {
224 this.account.getXmppConnection().sendIqPacket(
225 packet.generateResponse(IqPacket.TYPE.ERROR), null);
226 }
227 } else if (connected && payload.getName().equals("data")) {
228 this.receiveNextBlock(payload.getContent());
229 this.account.getXmppConnection().sendIqPacket(
230 packet.generateResponse(IqPacket.TYPE.RESULT), null);
231 } else {
232 // TODO some sort of exception
233 }
234 }
235}