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