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.utils.CryptoHelper;
19import eu.siacs.conversations.xml.Element;
20import eu.siacs.conversations.xmpp.OnIqPacketReceived;
21import eu.siacs.conversations.xmpp.stanzas.IqPacket;
22import rocks.xmpp.addr.Jid;
23
24public class JingleInbandTransport extends JingleTransport {
25
26 private Account account;
27 private Jid counterpart;
28 private int blockSize;
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 InputStream innerInputStream = null;
41 private OutputStream fileOutputStream = null;
42 private long remainingSize = 0;
43 private long fileSize = 0;
44 private MessageDigest digest;
45
46 private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged;
47
48 private OnIqPacketReceived onAckReceived = new OnIqPacketReceived() {
49 @Override
50 public void onIqPacketReceived(Account account, IqPacket packet) {
51 if (!connected) {
52 return;
53 }
54 if (packet.getType() == IqPacket.TYPE.RESULT) {
55 if (remainingSize > 0) {
56 sendNextBlock();
57 }
58 } else if (packet.getType() == IqPacket.TYPE.ERROR) {
59 onFileTransmissionStatusChanged.onFileTransferAborted();
60 }
61 }
62 };
63
64 public JingleInbandTransport(final JingleConnection connection, final String sid, final int blocksize) {
65 this.connection = connection;
66 this.account = connection.getAccount();
67 this.counterpart = connection.getCounterPart();
68 this.blockSize = blocksize;
69 this.sessionId = sid;
70 }
71
72 private void sendClose() {
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 sendClose();
182 file.setSha1Sum(digest.digest());
183 this.onFileTransmissionStatusChanged.onFileTransmitted(file);
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 file.setSha1Sum(digest.digest());
204 fileOutputStream.flush();
205 fileOutputStream.close();
206 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": receive next block nothing remaining");
207 this.onFileTransmissionStatusChanged.onFileTransmitted(file);
208 } else {
209 connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
210 }
211 } catch (Exception e) {
212 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage());
213 FileBackend.close(fileOutputStream);
214 this.onFileTransmissionStatusChanged.onFileTransferAborted();
215 }
216 }
217
218 public void deliverPayload(IqPacket packet, Element payload) {
219 if (payload.getName().equals("open")) {
220 if (!established) {
221 established = true;
222 connected = true;
223 this.receiveNextBlock("");
224 this.account.getXmppConnection().sendIqPacket(
225 packet.generateResponse(IqPacket.TYPE.RESULT), null);
226 } else {
227 this.account.getXmppConnection().sendIqPacket(
228 packet.generateResponse(IqPacket.TYPE.ERROR), null);
229 }
230 } else if (connected && payload.getName().equals("data")) {
231 this.receiveNextBlock(payload.getContent());
232 this.account.getXmppConnection().sendIqPacket(
233 packet.generateResponse(IqPacket.TYPE.RESULT), null);
234 } else if (connected && payload.getName().equals("close")) {
235 this.connected = false;
236 this.account.getXmppConnection().sendIqPacket(
237 packet.generateResponse(IqPacket.TYPE.RESULT), null);
238 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received ibb close");
239 } else {
240 Log.d(Config.LOGTAG, payload.toString());
241 // TODO some sort of exception
242 }
243 }
244}