1package eu.siacs.conversations.xmpp.jingle;
2
3import android.util.Base64;
4import android.util.Log;
5
6import com.google.common.base.Preconditions;
7
8import java.io.IOException;
9import java.io.InputStream;
10import java.io.OutputStream;
11import java.security.MessageDigest;
12import java.security.NoSuchAlgorithmException;
13import java.util.Arrays;
14
15import eu.siacs.conversations.Config;
16import eu.siacs.conversations.entities.Account;
17import eu.siacs.conversations.entities.DownloadableFile;
18import eu.siacs.conversations.persistance.FileBackend;
19import eu.siacs.conversations.services.AbstractConnectionManager;
20import eu.siacs.conversations.xml.Element;
21import eu.siacs.conversations.xmpp.Jid;
22import eu.siacs.conversations.xmpp.OnIqPacketReceived;
23import eu.siacs.conversations.xmpp.stanzas.IqPacket;
24
25public class JingleInBandTransport extends JingleTransport {
26
27 private final Account account;
28 private final Jid counterpart;
29 private final int blockSize;
30 private int seq = 0;
31 private final String sessionId;
32
33 private boolean established = false;
34
35 private boolean connected = true;
36
37 private DownloadableFile file;
38 private final JingleFileTransferConnection connection;
39
40 private InputStream fileInputStream = null;
41 private InputStream innerInputStream = null;
42 private OutputStream fileOutputStream = null;
43 private long remainingSize = 0;
44 private long fileSize = 0;
45 private MessageDigest digest;
46
47 private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged;
48
49 private final OnIqPacketReceived onAckReceived = new OnIqPacketReceived() {
50 @Override
51 public void onIqPacketReceived(Account account, IqPacket packet) {
52 if (!connected) {
53 return;
54 }
55 if (packet.getType() == IqPacket.TYPE.RESULT) {
56 if (remainingSize > 0) {
57 sendNextBlock();
58 }
59 } else if (packet.getType() == IqPacket.TYPE.ERROR) {
60 onFileTransmissionStatusChanged.onFileTransferAborted();
61 }
62 }
63 };
64
65 JingleInBandTransport(final JingleFileTransferConnection connection, final String sid, final int blockSize) {
66 this.connection = connection;
67 this.account = connection.getId().account;
68 this.counterpart = connection.getId().with;
69 this.blockSize = blockSize;
70 this.sessionId = sid;
71 }
72
73 private void sendClose() {
74 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending ibb close");
75 IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
76 iq.setTo(this.counterpart);
77 Element close = iq.addChild("close", "http://jabber.org/protocol/ibb");
78 close.setAttribute("sid", this.sessionId);
79 this.account.getXmppConnection().sendIqPacket(iq, null);
80 }
81
82 public boolean matches(final Account account, final String sessionId) {
83 return this.account == account && this.sessionId.equals(sessionId);
84 }
85
86 public void connect(final OnTransportConnected callback) {
87 IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
88 iq.setTo(this.counterpart);
89 Element open = iq.addChild("open", "http://jabber.org/protocol/ibb");
90 open.setAttribute("sid", this.sessionId);
91 open.setAttribute("stanza", "iq");
92 open.setAttribute("block-size", Integer.toString(this.blockSize));
93 this.connected = true;
94 this.account.getXmppConnection().sendIqPacket(iq, (account, packet) -> {
95 if (packet.getType() != IqPacket.TYPE.RESULT) {
96 callback.failed();
97 } else {
98 callback.established();
99 }
100 });
101 }
102
103 @Override
104 public void receive(DownloadableFile file, OnFileTransmissionStatusChanged callback) {
105 this.onFileTransmissionStatusChanged = Preconditions.checkNotNull(callback);
106 this.file = file;
107 try {
108 this.digest = MessageDigest.getInstance("SHA-1");
109 digest.reset();
110 this.fileOutputStream = connection.getFileOutputStream();
111 if (this.fileOutputStream == null) {
112 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not create output stream");
113 callback.onFileTransferAborted();
114 return;
115 }
116 this.remainingSize = this.fileSize = file.getExpectedSize();
117 } catch (final NoSuchAlgorithmException | IOException e) {
118 Log.d(Config.LOGTAG, account.getJid().asBareJid() + " " + e.getMessage());
119 callback.onFileTransferAborted();
120 }
121 }
122
123 @Override
124 public void send(DownloadableFile file, OnFileTransmissionStatusChanged callback) {
125 this.onFileTransmissionStatusChanged = Preconditions.checkNotNull(callback);
126 this.file = file;
127 try {
128 this.remainingSize = this.file.getExpectedSize();
129 this.fileSize = this.remainingSize;
130 this.digest = MessageDigest.getInstance("SHA-1");
131 this.digest.reset();
132 fileInputStream = connection.getFileInputStream();
133 if (fileInputStream == null) {
134 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could no create input stream");
135 callback.onFileTransferAborted();
136 return;
137 }
138 innerInputStream = AbstractConnectionManager.upgrade(file, fileInputStream);
139 if (this.connected) {
140 this.sendNextBlock();
141 }
142 } catch (Exception e) {
143 callback.onFileTransferAborted();
144 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage());
145 }
146 }
147
148 @Override
149 public void disconnect() {
150 this.connected = false;
151 FileBackend.close(fileOutputStream);
152 FileBackend.close(fileInputStream);
153 }
154
155 private void sendNextBlock() {
156 byte[] buffer = new byte[this.blockSize];
157 try {
158 int count = innerInputStream.read(buffer);
159 if (count == -1) {
160 sendClose();
161 file.setSha1Sum(digest.digest());
162 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sendNextBlock() count was -1");
163 this.onFileTransmissionStatusChanged.onFileTransmitted(file);
164 fileInputStream.close();
165 return;
166 } else if (count != buffer.length) {
167 int rem = innerInputStream.read(buffer, count, buffer.length - count);
168 if (rem > 0) {
169 count += rem;
170 }
171 }
172 this.remainingSize -= count;
173 this.digest.update(buffer, 0, count);
174 String base64 = Base64.encodeToString(buffer, 0, count, Base64.NO_WRAP);
175 IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
176 iq.setTo(this.counterpart);
177 Element data = iq.addChild("data", "http://jabber.org/protocol/ibb");
178 data.setAttribute("seq", Integer.toString(this.seq));
179 data.setAttribute("block-size", Integer.toString(this.blockSize));
180 data.setAttribute("sid", this.sessionId);
181 data.setContent(base64);
182 this.account.getXmppConnection().sendIqPacket(iq, this.onAckReceived);
183 this.account.getXmppConnection().r(); //don't fill up stanza queue too much
184 this.seq++;
185 connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
186 if (this.remainingSize <= 0) {
187 file.setSha1Sum(digest.digest());
188 this.onFileTransmissionStatusChanged.onFileTransmitted(file);
189 sendClose();
190 fileInputStream.close();
191 }
192 } catch (IOException e) {
193 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": io exception during sendNextBlock() " + e.getMessage());
194 FileBackend.close(fileInputStream);
195 this.onFileTransmissionStatusChanged.onFileTransferAborted();
196 }
197 }
198
199 private void receiveNextBlock(String data) {
200 try {
201 byte[] buffer = Base64.decode(data, Base64.NO_WRAP);
202 if (this.remainingSize < buffer.length) {
203 buffer = Arrays.copyOfRange(buffer, 0, (int) this.remainingSize);
204 }
205 this.remainingSize -= buffer.length;
206 this.fileOutputStream.write(buffer);
207 this.digest.update(buffer);
208 if (this.remainingSize <= 0) {
209 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received last block. waiting for close");
210 } else {
211 connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
212 }
213 } catch (Exception e) {
214 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage(), e);
215 FileBackend.close(fileOutputStream);
216 this.onFileTransmissionStatusChanged.onFileTransferAborted();
217 }
218 }
219
220 private void done() {
221 try {
222 file.setSha1Sum(digest.digest());
223 fileOutputStream.flush();
224 fileOutputStream.close();
225 this.onFileTransmissionStatusChanged.onFileTransmitted(file);
226 } catch (Exception e) {
227 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage());
228 FileBackend.close(fileOutputStream);
229 this.onFileTransmissionStatusChanged.onFileTransferAborted();
230 }
231 }
232
233 void deliverPayload(IqPacket packet, Element payload) {
234 if (payload.getName().equals("open")) {
235 if (!established) {
236 established = true;
237 connected = true;
238 this.receiveNextBlock("");
239 this.account.getXmppConnection().sendIqPacket(
240 packet.generateResponse(IqPacket.TYPE.RESULT), null);
241 } else {
242 this.account.getXmppConnection().sendIqPacket(
243 packet.generateResponse(IqPacket.TYPE.ERROR), null);
244 }
245 } else if (connected && payload.getName().equals("data")) {
246 this.receiveNextBlock(payload.getContent());
247 this.account.getXmppConnection().sendIqPacket(
248 packet.generateResponse(IqPacket.TYPE.RESULT), null);
249 } else if (connected && payload.getName().equals("close")) {
250 this.connected = false;
251 this.account.getXmppConnection().sendIqPacket(
252 packet.generateResponse(IqPacket.TYPE.RESULT), null);
253 if (this.remainingSize <= 0) {
254 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received ibb close. done");
255 done();
256 } else {
257 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received ibb close with " + this.remainingSize + " remaining");
258 FileBackend.close(fileOutputStream);
259 this.onFileTransmissionStatusChanged.onFileTransferAborted();
260 }
261 } else {
262 this.account.getXmppConnection().sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null);
263 }
264 }
265}