1package eu.siacs.conversations.xmpp.jingle;
2
3import java.io.IOException;
4import java.io.InputStream;
5import java.io.OutputStream;
6import java.security.MessageDigest;
7import java.security.NoSuchAlgorithmException;
8import java.util.Arrays;
9
10import android.util.Base64;
11import eu.siacs.conversations.entities.Account;
12import eu.siacs.conversations.entities.DownloadableFile;
13import eu.siacs.conversations.utils.CryptoHelper;
14import eu.siacs.conversations.xml.Element;
15import eu.siacs.conversations.xmpp.OnIqPacketReceived;
16import eu.siacs.conversations.xmpp.jid.Jid;
17import eu.siacs.conversations.xmpp.stanzas.IqPacket;
18
19public class JingleInbandTransport extends JingleTransport {
20
21 private Account account;
22 private Jid counterpart;
23 private int blockSize;
24 private int bufferSize;
25 private int seq = 0;
26 private String sessionId;
27
28 private boolean established = false;
29
30 private DownloadableFile file;
31
32 private InputStream fileInputStream = null;
33 private OutputStream fileOutputStream;
34 private long remainingSize;
35 private MessageDigest digest;
36
37 private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged;
38
39 private OnIqPacketReceived onAckReceived = new OnIqPacketReceived() {
40 @Override
41 public void onIqPacketReceived(Account account, IqPacket packet) {
42 if (packet.getType() == IqPacket.TYPE_RESULT) {
43 sendNextBlock();
44 }
45 }
46 };
47
48 public JingleInbandTransport(final Account account, final Jid counterpart,
49 final String sid, final int blocksize) {
50 this.account = account;
51 this.counterpart = counterpart;
52 this.blockSize = blocksize;
53 this.bufferSize = blocksize / 4;
54 this.sessionId = sid;
55 }
56
57 public void connect(final OnTransportConnected callback) {
58 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
59 iq.setTo(this.counterpart);
60 Element open = iq.addChild("open", "http://jabber.org/protocol/ibb");
61 open.setAttribute("sid", this.sessionId);
62 open.setAttribute("stanza", "iq");
63 open.setAttribute("block-size", Integer.toString(this.blockSize));
64
65 this.account.getXmppConnection().sendIqPacket(iq,
66 new OnIqPacketReceived() {
67
68 @Override
69 public void onIqPacketReceived(Account account,
70 IqPacket packet) {
71 if (packet.getType() == IqPacket.TYPE_ERROR) {
72 callback.failed();
73 } else {
74 callback.established();
75 }
76 }
77 });
78 }
79
80 @Override
81 public void receive(DownloadableFile file,
82 OnFileTransmissionStatusChanged callback) {
83 this.onFileTransmissionStatusChanged = callback;
84 this.file = file;
85 try {
86 this.digest = MessageDigest.getInstance("SHA-1");
87 digest.reset();
88 file.getParentFile().mkdirs();
89 file.createNewFile();
90 this.fileOutputStream = file.createOutputStream();
91 if (this.fileOutputStream == null) {
92 callback.onFileTransferAborted();
93 return;
94 }
95 this.remainingSize = file.getExpectedSize();
96 } catch (final NoSuchAlgorithmException | IOException e) {
97 callback.onFileTransferAborted();
98 }
99 }
100
101 @Override
102 public void send(DownloadableFile file,
103 OnFileTransmissionStatusChanged callback) {
104 this.onFileTransmissionStatusChanged = callback;
105 this.file = file;
106 try {
107 this.digest = MessageDigest.getInstance("SHA-1");
108 this.digest.reset();
109 fileInputStream = this.file.createInputStream();
110 if (fileInputStream == null) {
111 callback.onFileTransferAborted();
112 return;
113 }
114 this.sendNextBlock();
115 } catch (NoSuchAlgorithmException e) {
116 callback.onFileTransferAborted();
117 }
118 }
119
120 private void sendNextBlock() {
121 byte[] buffer = new byte[this.bufferSize];
122 try {
123 int count = fileInputStream.read(buffer);
124 if (count == -1) {
125 file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
126 fileInputStream.close();
127 this.onFileTransmissionStatusChanged.onFileTransmitted(file);
128 } else {
129 this.digest.update(buffer);
130 String base64 = Base64.encodeToString(buffer, Base64.NO_WRAP);
131 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
132 iq.setTo(this.counterpart);
133 Element data = iq.addChild("data",
134 "http://jabber.org/protocol/ibb");
135 data.setAttribute("seq", Integer.toString(this.seq));
136 data.setAttribute("block-size",
137 Integer.toString(this.blockSize));
138 data.setAttribute("sid", this.sessionId);
139 data.setContent(base64);
140 this.account.getXmppConnection().sendIqPacket(iq,
141 this.onAckReceived);
142 this.seq++;
143 }
144 } catch (IOException e) {
145 this.onFileTransmissionStatusChanged.onFileTransferAborted();
146 }
147 }
148
149 private void receiveNextBlock(String data) {
150 try {
151 byte[] buffer = Base64.decode(data, Base64.NO_WRAP);
152 if (this.remainingSize < buffer.length) {
153 buffer = Arrays
154 .copyOfRange(buffer, 0, (int) this.remainingSize);
155 }
156 this.remainingSize -= buffer.length;
157
158 this.fileOutputStream.write(buffer);
159
160 this.digest.update(buffer);
161 if (this.remainingSize <= 0) {
162 file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
163 fileOutputStream.flush();
164 fileOutputStream.close();
165 this.onFileTransmissionStatusChanged.onFileTransmitted(file);
166 }
167 } catch (IOException e) {
168 this.onFileTransmissionStatusChanged.onFileTransferAborted();
169 }
170 }
171
172 public void deliverPayload(IqPacket packet, Element payload) {
173 if (payload.getName().equals("open")) {
174 if (!established) {
175 established = true;
176 this.account.getXmppConnection().sendIqPacket(
177 packet.generateRespone(IqPacket.TYPE_RESULT), null);
178 } else {
179 this.account.getXmppConnection().sendIqPacket(
180 packet.generateRespone(IqPacket.TYPE_ERROR), null);
181 }
182 } else if (payload.getName().equals("data")) {
183 this.receiveNextBlock(payload.getContent());
184 this.account.getXmppConnection().sendIqPacket(
185 packet.generateRespone(IqPacket.TYPE_RESULT), null);
186 } else {
187 // TODO some sort of exception
188 }
189 }
190}