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