1package eu.siacs.conversations.xmpp.jingle;
2
3import java.util.ArrayList;
4import java.util.HashMap;
5import java.util.Iterator;
6import java.util.List;
7import java.util.Map.Entry;
8
9import android.util.Log;
10
11import eu.siacs.conversations.entities.Account;
12import eu.siacs.conversations.entities.Conversation;
13import eu.siacs.conversations.entities.Message;
14import eu.siacs.conversations.services.XmppConnectionService;
15import eu.siacs.conversations.xml.Element;
16import eu.siacs.conversations.xmpp.OnIqPacketReceived;
17import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
18import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
19import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
20import eu.siacs.conversations.xmpp.stanzas.IqPacket;
21
22public class JingleConnection {
23
24 private JingleConnectionManager mJingleConnectionManager;
25 private XmppConnectionService mXmppConnectionService;
26
27 public static final int STATUS_INITIATED = 0;
28 public static final int STATUS_ACCEPTED = 1;
29 public static final int STATUS_TERMINATED = 2;
30 public static final int STATUS_CANCELED = 3;
31 public static final int STATUS_FINISHED = 4;
32 public static final int STATUS_TRANSMITTING = 5;
33 public static final int STATUS_FAILED = 99;
34
35 private int status = -1;
36 private Message message;
37 private String sessionId;
38 private Account account;
39 private String initiator;
40 private String responder;
41 private List<Element> candidates = new ArrayList<Element>();
42 private List<String> candidatesUsedByCounterpart = new ArrayList<String>();
43 private HashMap<String, SocksConnection> connections = new HashMap<String, SocksConnection>();
44 private Content content = new Content();
45 private JingleFile file = null;
46
47 private OnIqPacketReceived responseListener = new OnIqPacketReceived() {
48
49 @Override
50 public void onIqPacketReceived(Account account, IqPacket packet) {
51 if (packet.getType() == IqPacket.TYPE_ERROR) {
52 mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
53 status = STATUS_FAILED;
54 }
55 }
56 };
57
58 public JingleConnection(JingleConnectionManager mJingleConnectionManager) {
59 this.mJingleConnectionManager = mJingleConnectionManager;
60 this.mXmppConnectionService = mJingleConnectionManager.getXmppConnectionService();
61 }
62
63 public String getSessionId() {
64 return this.sessionId;
65 }
66
67 public String getAccountJid() {
68 return this.account.getFullJid();
69 }
70
71 public String getCounterPart() {
72 return this.message.getCounterpart();
73 }
74
75 public void deliverPacket(JinglePacket packet) {
76
77 if (packet.isAction("session-terminate")) {
78 Reason reason = packet.getReason();
79 if (reason.hasChild("cancel")) {
80 this.cancel();
81 } else if (reason.hasChild("success")) {
82 this.finish();
83 }
84 } else if (packet.isAction("session-accept")) {
85 accept(packet);
86 } else if (packet.isAction("transport-info")) {
87 transportInfo(packet);
88 } else {
89 Log.d("xmppService","packet arrived in connection. action was "+packet.getAction());
90 }
91 }
92
93 public void init(Message message) {
94 this.message = message;
95 this.account = message.getConversation().getAccount();
96 this.initiator = this.account.getFullJid();
97 this.responder = this.message.getCounterpart();
98 this.sessionId = this.mJingleConnectionManager.nextRandomId();
99 if (this.candidates.size() > 0) {
100 this.sendInitRequest();
101 } else {
102 this.mJingleConnectionManager.getPrimaryCandidate(account, new OnPrimaryCandidateFound() {
103
104 @Override
105 public void onPrimaryCandidateFound(boolean success, Element candidate) {
106 if (success) {
107 mergeCandidate(candidate);
108 }
109 sendInitRequest();
110 }
111 });
112 }
113
114 }
115
116 public void init(Account account, JinglePacket packet) {
117 this.status = STATUS_INITIATED;
118 Conversation conversation = this.mXmppConnectionService.findOrCreateConversation(account, packet.getFrom().split("/")[0], false);
119 this.message = new Message(conversation, "receiving image file", Message.ENCRYPTION_NONE);
120 this.message.setType(Message.TYPE_IMAGE);
121 this.message.setStatus(Message.STATUS_RECIEVING);
122 String[] fromParts = packet.getFrom().split("/");
123 this.message.setPresence(fromParts[1]);
124 this.account = account;
125 this.initiator = packet.getFrom();
126 this.responder = this.account.getFullJid();
127 this.sessionId = packet.getSessionId();
128 this.content = packet.getJingleContent();
129 this.mergeCandidates(this.content.getCanditates());
130 Element fileOffer = packet.getJingleContent().getFileOffer();
131 if (fileOffer!=null) {
132 this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message);
133 Element fileSize = fileOffer.findChild("size");
134 Element fileName = fileOffer.findChild("name");
135 this.file.setExpectedSize(Long.parseLong(fileSize.getContent()));
136 conversation.getMessages().add(message);
137 this.mXmppConnectionService.databaseBackend.createMessage(message);
138 if (this.mXmppConnectionService.convChangedListener!=null) {
139 this.mXmppConnectionService.convChangedListener.onConversationListChanged();
140 }
141 if (this.file.getExpectedSize()>=this.mJingleConnectionManager.getAutoAcceptFileSize()) {
142 Log.d("xmppService","auto accepting file from "+packet.getFrom());
143 this.sendAccept();
144 } else {
145 Log.d("xmppService","not auto accepting new file offer with size: "+this.file.getExpectedSize()+" allowed size:"+this.mJingleConnectionManager.getAutoAcceptFileSize());
146 }
147 } else {
148 Log.d("xmppService","no file offer was attached. aborting");
149 }
150 }
151
152 private void sendInitRequest() {
153 JinglePacket packet = this.bootstrapPacket();
154 packet.setAction("session-initiate");
155 this.content = new Content();
156 if (message.getType() == Message.TYPE_IMAGE) {
157 content.setAttribute("creator", "initiator");
158 content.setAttribute("name", "a-file-offer");
159 this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message);
160 content.setFileOffer(this.file);
161 content.setCandidates(this.mJingleConnectionManager.nextRandomId(),this.candidates);
162 packet.setContent(content);
163 Log.d("xmppService",packet.toString());
164 account.getXmppConnection().sendIqPacket(packet, this.responseListener);
165 this.status = STATUS_INITIATED;
166 }
167 }
168
169 private void sendAccept() {
170 this.mJingleConnectionManager.getPrimaryCandidate(this.account, new OnPrimaryCandidateFound() {
171
172 @Override
173 public void onPrimaryCandidateFound(boolean success, Element candidate) {
174 if (success) {
175 if (!equalCandidateExists(candidate)) {
176 mergeCandidate(candidate);
177 content.addCandidate(candidate);
178 }
179 }
180 JinglePacket packet = bootstrapPacket();
181 packet.setAction("session-accept");
182 packet.setContent(content);
183 account.getXmppConnection().sendIqPacket(packet, new OnIqPacketReceived() {
184
185 @Override
186 public void onIqPacketReceived(Account account, IqPacket packet) {
187 if (packet.getType() != IqPacket.TYPE_ERROR) {
188 status = STATUS_ACCEPTED;
189 connectNextCandidate();
190 }
191 }
192 });
193 }
194 });
195
196 }
197
198 private JinglePacket bootstrapPacket() {
199 JinglePacket packet = new JinglePacket();
200 packet.setFrom(account.getFullJid());
201 packet.setTo(this.message.getCounterpart()); //fixme, not right in all cases;
202 packet.setSessionId(this.sessionId);
203 packet.setInitiator(this.initiator);
204 return packet;
205 }
206
207 private void accept(JinglePacket packet) {
208 Log.d("xmppService","session-accept: "+packet.toString());
209 Content content = packet.getJingleContent();
210 mergeCandidates(content.getCanditates());
211 this.status = STATUS_ACCEPTED;
212 this.connectNextCandidate();
213 IqPacket response = packet.generateRespone(IqPacket.TYPE_RESULT);
214 account.getXmppConnection().sendIqPacket(response, null);
215 }
216
217 private void transportInfo(JinglePacket packet) {
218 Content content = packet.getJingleContent();
219 String cid = content.getUsedCandidate();
220 IqPacket response = packet.generateRespone(IqPacket.TYPE_RESULT);
221 if (cid!=null) {
222 Log.d("xmppService","candidate used by counterpart:"+cid);
223 this.candidatesUsedByCounterpart.add(cid);
224 if (this.connections.containsKey(cid)) {
225 SocksConnection connection = this.connections.get(cid);
226 if (connection.isEstablished()) {
227 if (status!=STATUS_TRANSMITTING) {
228 this.connect(connection);
229 } else {
230 Log.d("xmppService","ignoring canditate used because we are already transmitting");
231 }
232 } else {
233 Log.d("xmppService","not yet connected. check when callback comes back");
234 }
235 } else {
236 Log.d("xmppService","candidate not yet in list of connections");
237 }
238 }
239 account.getXmppConnection().sendIqPacket(response, null);
240 }
241
242 private void connect(final SocksConnection connection) {
243 this.status = STATUS_TRANSMITTING;
244 final OnFileTransmitted callback = new OnFileTransmitted() {
245
246 @Override
247 public void onFileTransmitted(JingleFile file) {
248 if (initiator.equals(account.getFullJid())) {
249 mXmppConnectionService.markMessage(message, Message.STATUS_SEND);
250 } else {
251 mXmppConnectionService.markMessage(message, Message.STATUS_RECIEVED);
252 }
253 Log.d("xmppService","sucessfully transmitted file. sha1:"+file.getSha1Sum());
254 }
255 };
256 if ((connection.isProxy()&&(connection.getCid().equals(mJingleConnectionManager.getPrimaryCandidateId(account))))) {
257 Log.d("xmppService","candidate "+connection.getCid()+" was our proxy and needs activation");
258 IqPacket activation = new IqPacket(IqPacket.TYPE_SET);
259 activation.setTo(connection.getJid());
260 activation.query("http://jabber.org/protocol/bytestreams").setAttribute("sid", this.getSessionId());
261 activation.query().addChild("activate").setContent(this.getResponder());
262 this.account.getXmppConnection().sendIqPacket(activation, new OnIqPacketReceived() {
263
264 @Override
265 public void onIqPacketReceived(Account account, IqPacket packet) {
266 Log.d("xmppService","activation result: "+packet.toString());
267 if (initiator.equals(account.getFullJid())) {
268 Log.d("xmppService","we were initiating. sending file");
269 connection.send(file,callback);
270 } else {
271 connection.receive(file,callback);
272 Log.d("xmppService","we were responding. receiving file");
273 }
274 }
275 });
276 } else {
277 if (initiator.equals(account.getFullJid())) {
278 Log.d("xmppService","we were initiating. sending file");
279 connection.send(file,callback);
280 } else {
281 Log.d("xmppService","we were responding. receiving file");
282 connection.receive(file,callback);
283 }
284 }
285 }
286
287 private void finish() {
288 this.status = STATUS_FINISHED;
289 this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND);
290 this.disconnect();
291 }
292
293 public void cancel() {
294 this.disconnect();
295 this.status = STATUS_CANCELED;
296 this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND_REJECTED);
297 }
298
299 private void connectNextCandidate() {
300 for(Element candidate : this.candidates) {
301 String cid = candidate.getAttribute("cid");
302 if (!connections.containsKey(cid)) {
303 this.connectWithCandidate(candidate);
304 break;
305 }
306 }
307 }
308
309 private void connectWithCandidate(Element candidate) {
310 final SocksConnection socksConnection = new SocksConnection(this,candidate);
311 connections.put(socksConnection.getCid(), socksConnection);
312 socksConnection.connect(new OnSocksConnection() {
313
314 @Override
315 public void failed() {
316 connectNextCandidate();
317 }
318
319 @Override
320 public void established() {
321 if (candidatesUsedByCounterpart.contains(socksConnection.getCid())) {
322 if (status!=STATUS_TRANSMITTING) {
323 connect(socksConnection);
324 } else {
325 Log.d("xmppService","ignoring cuz already transmitting");
326 }
327 } else {
328 sendCandidateUsed(socksConnection.getCid());
329 }
330 }
331 });
332 }
333
334 private void disconnect() {
335 Iterator<Entry<String, SocksConnection>> it = this.connections.entrySet().iterator();
336 while (it.hasNext()) {
337 Entry<String, SocksConnection> pairs = it.next();
338 pairs.getValue().disconnect();
339 it.remove();
340 }
341 }
342
343 private void sendCandidateUsed(final String cid) {
344 JinglePacket packet = bootstrapPacket();
345 packet.setAction("transport-info");
346 Content content = new Content();
347 content.setUsedCandidate(this.content.getTransportId(), cid);
348 packet.setContent(content);
349 Log.d("xmppService","send using candidate: "+cid);
350 this.account.getXmppConnection().sendIqPacket(packet, new OnIqPacketReceived() {
351
352 @Override
353 public void onIqPacketReceived(Account account, IqPacket packet) {
354 Log.d("xmppService","got ack for our candidate used");
355 if (status!=STATUS_TRANSMITTING) {
356 connect(connections.get(cid));
357 } else {
358 Log.d("xmppService","ignoring cuz already transmitting");
359 }
360 }
361 });
362 }
363
364 public String getInitiator() {
365 return this.initiator;
366 }
367
368 public String getResponder() {
369 return this.responder;
370 }
371
372 public int getStatus() {
373 return this.status;
374 }
375
376 private boolean equalCandidateExists(Element candidate) {
377 for(Element c : this.candidates) {
378 if (c.getAttribute("host").equals(candidate.getAttribute("host"))&&(c.getAttribute("port").equals(candidate.getAttribute("port")))) {
379 return true;
380 }
381 }
382 return false;
383 }
384
385 private void mergeCandidate(Element candidate) {
386 for(Element c : this.candidates) {
387 if (c.getAttribute("cid").equals(candidate.getAttribute("cid"))) {
388 return;
389 }
390 }
391 this.candidates.add(candidate);
392 }
393
394 private void mergeCandidates(List<Element> candidates) {
395 for(Element c : candidates) {
396 mergeCandidate(c);
397 }
398 }
399
400 private Element getCandidate(String cid) {
401 for(Element c : this.candidates) {
402 if (c.getAttribute("cid").equals(cid)) {
403 return c;
404 }
405 }
406 return null;
407 }
408}