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