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.services.AbstractConnectionManager;
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 final Account account;
26 private final Jid counterpart;
27 private final int blockSize;
28 private int seq = 0;
29 private final String sessionId;
30
31 private boolean established = false;
32
33 private boolean connected = true;
34
35 private DownloadableFile file;
36 private final JingleConnection connection;
37
38 private InputStream fileInputStream = null;
39 private InputStream innerInputStream = 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) {
51 return;
52 }
53 if (packet.getType() == IqPacket.TYPE.RESULT) {
54 if (remainingSize > 0) {
55 sendNextBlock();
56 }
57 } else if (packet.getType() == IqPacket.TYPE.ERROR) {
58 onFileTransmissionStatusChanged.onFileTransferAborted();
59 }
60 }
61 };
62
63 JingleInBandTransport(final JingleConnection connection, final String sid, final int blockSize) {
64 this.connection = connection;
65 this.account = connection.getAccount();
66 this.counterpart = connection.getCounterPart();
67 this.blockSize = blockSize;
68 this.sessionId = sid;
69 }
70
71 private void sendClose() {
72 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending ibb close");
73 IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
74 iq.setTo(this.counterpart);
75 Element close = iq.addChild("close", "http://jabber.org/protocol/ibb");
76 close.setAttribute("sid", this.sessionId);
77 this.account.getXmppConnection().sendIqPacket(iq, null);
78 }
79
80 public void connect(final OnTransportConnected callback) {
81 IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
82 iq.setTo(this.counterpart);
83 Element open = iq.addChild("open", "http://jabber.org/protocol/ibb");
84 open.setAttribute("sid", this.sessionId);
85 open.setAttribute("stanza", "iq");
86 open.setAttribute("block-size", Integer.toString(this.blockSize));
87 this.connected = true;
88 this.account.getXmppConnection().sendIqPacket(iq, (account, packet) -> {
89 if (packet.getType() != IqPacket.TYPE.RESULT) {
90 callback.failed();
91 } else {
92 callback.established();
93 }
94 });
95 }
96
97 @Override
98 public void receive(DownloadableFile file, OnFileTransmissionStatusChanged callback) {
99 this.onFileTransmissionStatusChanged = callback;
100 this.file = file;
101 try {
102 this.digest = MessageDigest.getInstance("SHA-1");
103 digest.reset();
104 this.fileOutputStream = connection.getFileOutputStream();
105 if (this.fileOutputStream == null) {
106 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not create output stream");
107 callback.onFileTransferAborted();
108 return;
109 }
110 this.remainingSize = this.fileSize = file.getExpectedSize();
111 } catch (final NoSuchAlgorithmException | IOException e) {
112 Log.d(Config.LOGTAG, account.getJid().asBareJid() + " " + e.getMessage());
113 callback.onFileTransferAborted();
114 }
115 }
116
117 @Override
118 public void send(DownloadableFile file, OnFileTransmissionStatusChanged callback) {
119 this.onFileTransmissionStatusChanged = callback;
120 this.file = file;
121 try {
122 this.remainingSize = this.file.getExpectedSize();
123 this.fileSize = this.remainingSize;
124 this.digest = MessageDigest.getInstance("SHA-1");
125 this.digest.reset();
126 fileInputStream = connection.getFileInputStream();
127 if (fileInputStream == null) {
128 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could no create input stream");
129 callback.onFileTransferAborted();
130 return;
131 }
132 innerInputStream = AbstractConnectionManager.upgrade(file, fileInputStream);
133 if (this.connected) {
134 this.sendNextBlock();
135 }
136 } catch (Exception e) {
137 callback.onFileTransferAborted();
138 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage());
139 }
140 }
141
142 @Override
143 public void disconnect() {
144 this.connected = false;
145 FileBackend.close(fileOutputStream);
146 FileBackend.close(fileInputStream);
147 }
148
149 private void sendNextBlock() {
150 byte[] buffer = new byte[this.blockSize];
151 try {
152 int count = innerInputStream.read(buffer);
153 if (count == -1) {
154 sendClose();
155 file.setSha1Sum(digest.digest());
156 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sendNextBlock() count was -1");
157 this.onFileTransmissionStatusChanged.onFileTransmitted(file);
158 fileInputStream.close();
159 return;
160 } else if (count != buffer.length) {
161 int rem = innerInputStream.read(buffer, count, buffer.length - count);
162 if (rem > 0) {
163 count += rem;
164 }
165 }
166 this.remainingSize -= count;
167 this.digest.update(buffer, 0, count);
168 String base64 = Base64.encodeToString(buffer, 0, count, Base64.NO_WRAP);
169 IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
170 iq.setTo(this.counterpart);
171 Element data = iq.addChild("data", "http://jabber.org/protocol/ibb");
172 data.setAttribute("seq", Integer.toString(this.seq));
173 data.setAttribute("block-size", Integer.toString(this.blockSize));
174 data.setAttribute("sid", this.sessionId);
175 data.setContent(base64);
176 this.account.getXmppConnection().sendIqPacket(iq, this.onAckReceived);
177 this.account.getXmppConnection().r(); //don't fill up stanza queue too much
178 this.seq++;
179 connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
180 if (this.remainingSize <= 0) {
181 file.setSha1Sum(digest.digest());
182 this.onFileTransmissionStatusChanged.onFileTransmitted(file);
183 sendClose();
184 fileInputStream.close();
185 }
186 } catch (IOException e) {
187 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": io exception during sendNextBlock() " + e.getMessage());
188 FileBackend.close(fileInputStream);
189 this.onFileTransmissionStatusChanged.onFileTransferAborted();
190 }
191 }
192
193 private void receiveNextBlock(String data) {
194 try {
195 byte[] buffer = Base64.decode(data, Base64.NO_WRAP);
196 if (this.remainingSize < buffer.length) {
197 buffer = Arrays.copyOfRange(buffer, 0, (int) this.remainingSize);
198 }
199 this.remainingSize -= buffer.length;
200 this.fileOutputStream.write(buffer);
201 this.digest.update(buffer);
202 if (this.remainingSize <= 0) {
203 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received last block. waiting for close");
204 } else {
205 connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
206 }
207 } catch (Exception e) {
208 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage());
209 FileBackend.close(fileOutputStream);
210 this.onFileTransmissionStatusChanged.onFileTransferAborted();
211 }
212 }
213
214 private void done() {
215 try {
216 file.setSha1Sum(digest.digest());
217 fileOutputStream.flush();
218 fileOutputStream.close();
219 this.onFileTransmissionStatusChanged.onFileTransmitted(file);
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 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 if (this.remainingSize <= 0) {
248 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received ibb close. done");
249 done();
250 } else {
251 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received ibb close with " + this.remainingSize + " remaining");
252 FileBackend.close(fileOutputStream);
253 this.onFileTransmissionStatusChanged.onFileTransferAborted();
254 }
255 } else {
256 this.account.getXmppConnection().sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null);
257 }
258 }
259}