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