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;
11
12import eu.siacs.conversations.entities.Account;
13import eu.siacs.conversations.entities.DownloadableFile;
14import eu.siacs.conversations.persistance.FileBackend;
15import eu.siacs.conversations.utils.CryptoHelper;
16import eu.siacs.conversations.xml.Element;
17import eu.siacs.conversations.xmpp.OnIqPacketReceived;
18import eu.siacs.conversations.xmpp.jid.Jid;
19import eu.siacs.conversations.xmpp.stanzas.IqPacket;
20
21public class JingleInbandTransport extends JingleTransport {
22
23 private Account account;
24 private Jid counterpart;
25 private int blockSize;
26 private int bufferSize;
27 private int seq = 0;
28 private String sessionId;
29
30 private boolean established = false;
31
32 private boolean connected = true;
33
34 private DownloadableFile file;
35 private JingleConnection connection;
36
37 private InputStream fileInputStream = null;
38 private OutputStream fileOutputStream = null;
39 private long remainingSize = 0;
40 private long fileSize = 0;
41 private MessageDigest digest;
42
43 private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged;
44
45 private OnIqPacketReceived onAckReceived = new OnIqPacketReceived() {
46 @Override
47 public void onIqPacketReceived(Account account, IqPacket packet) {
48 if (connected && packet.getType() == IqPacket.TYPE.RESULT) {
49 sendNextBlock();
50 }
51 }
52 };
53
54 public JingleInbandTransport(final JingleConnection connection, final String sid, final int blocksize) {
55 this.connection = connection;
56 this.account = connection.getAccount();
57 this.counterpart = connection.getCounterPart();
58 this.blockSize = blocksize;
59 this.bufferSize = blocksize / 4;
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 callback.onFileTransferAborted();
99 return;
100 }
101 this.remainingSize = this.fileSize = file.getExpectedSize();
102 } catch (final NoSuchAlgorithmException | IOException e) {
103 callback.onFileTransferAborted();
104 }
105 }
106
107 @Override
108 public void send(DownloadableFile file,
109 OnFileTransmissionStatusChanged callback) {
110 this.onFileTransmissionStatusChanged = callback;
111 this.file = file;
112 try {
113 this.remainingSize = this.file.getSize();
114 this.fileSize = this.remainingSize;
115 this.digest = MessageDigest.getInstance("SHA-1");
116 this.digest.reset();
117 fileInputStream = this.file.createInputStream();
118 if (fileInputStream == null) {
119 callback.onFileTransferAborted();
120 return;
121 }
122 if (this.connected) {
123 this.sendNextBlock();
124 }
125 } catch (NoSuchAlgorithmException e) {
126 callback.onFileTransferAborted();
127 }
128 }
129
130 @Override
131 public void disconnect() {
132 this.connected = false;
133 if (this.fileOutputStream != null) {
134 try {
135 this.fileOutputStream.close();
136 } catch (IOException e) {
137
138 }
139 }
140 if (this.fileInputStream != null) {
141 try {
142 this.fileInputStream.close();
143 } catch (IOException e) {
144
145 }
146 }
147 }
148
149 private void sendNextBlock() {
150 byte[] buffer = new byte[this.bufferSize];
151 try {
152 int count = fileInputStream.read(buffer);
153 if (count == -1) {
154 file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
155 fileInputStream.close();
156 this.onFileTransmissionStatusChanged.onFileTransmitted(file);
157 } else {
158 this.remainingSize -= count;
159 this.digest.update(buffer);
160 String base64 = Base64.encodeToString(buffer, Base64.NO_WRAP);
161 IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
162 iq.setTo(this.counterpart);
163 Element data = iq.addChild("data",
164 "http://jabber.org/protocol/ibb");
165 data.setAttribute("seq", Integer.toString(this.seq));
166 data.setAttribute("block-size",
167 Integer.toString(this.blockSize));
168 data.setAttribute("sid", this.sessionId);
169 data.setContent(base64);
170 this.account.getXmppConnection().sendIqPacket(iq,
171 this.onAckReceived);
172 this.seq++;
173 connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
174 }
175 } catch (IOException e) {
176 FileBackend.close(fileInputStream);
177 this.onFileTransmissionStatusChanged.onFileTransferAborted();
178 }
179 }
180
181 private void receiveNextBlock(String data) {
182 try {
183 byte[] buffer = Base64.decode(data, Base64.NO_WRAP);
184 if (this.remainingSize < buffer.length) {
185 buffer = Arrays
186 .copyOfRange(buffer, 0, (int) this.remainingSize);
187 }
188 this.remainingSize -= buffer.length;
189
190
191 this.fileOutputStream.write(buffer);
192
193 this.digest.update(buffer);
194 if (this.remainingSize <= 0) {
195 file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
196 fileOutputStream.flush();
197 fileOutputStream.close();
198 this.onFileTransmissionStatusChanged.onFileTransmitted(file);
199 } else {
200 connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
201 }
202 } catch (IOException e) {
203 FileBackend.close(fileOutputStream);
204 this.onFileTransmissionStatusChanged.onFileTransferAborted();
205 }
206 }
207
208 public void deliverPayload(IqPacket packet, Element payload) {
209 if (payload.getName().equals("open")) {
210 if (!established) {
211 established = true;
212 connected = true;
213 this.receiveNextBlock("");
214 this.account.getXmppConnection().sendIqPacket(
215 packet.generateResponse(IqPacket.TYPE.RESULT), null);
216 } else {
217 this.account.getXmppConnection().sendIqPacket(
218 packet.generateResponse(IqPacket.TYPE.ERROR), null);
219 }
220 } else if (connected && payload.getName().equals("data")) {
221 this.receiveNextBlock(payload.getContent());
222 this.account.getXmppConnection().sendIqPacket(
223 packet.generateResponse(IqPacket.TYPE.RESULT), null);
224 } else {
225 // TODO some sort of exception
226 }
227 }
228}