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<JingleCandidate> candidates = new ArrayList<JingleCandidate>();
42 private HashMap<String, SocksConnection> connections = new HashMap<String, SocksConnection>();
43
44 private String transportId;
45 private Element fileOffer;
46 private JingleFile file = null;
47
48 private boolean receivedCandidate = false;
49 private boolean sentCandidate = false;
50
51 private OnIqPacketReceived responseListener = new OnIqPacketReceived() {
52
53 @Override
54 public void onIqPacketReceived(Account account, IqPacket packet) {
55 if (packet.getType() == IqPacket.TYPE_ERROR) {
56 mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
57 status = STATUS_FAILED;
58 }
59 }
60 };
61
62 public JingleConnection(JingleConnectionManager mJingleConnectionManager) {
63 this.mJingleConnectionManager = mJingleConnectionManager;
64 this.mXmppConnectionService = mJingleConnectionManager.getXmppConnectionService();
65 }
66
67 public String getSessionId() {
68 return this.sessionId;
69 }
70
71 public String getAccountJid() {
72 return this.account.getFullJid();
73 }
74
75 public String getCounterPart() {
76 return this.message.getCounterpart();
77 }
78
79 public void deliverPacket(JinglePacket packet) {
80
81 if (packet.isAction("session-terminate")) {
82 Reason reason = packet.getReason();
83 if (reason!=null) {
84 if (reason.hasChild("cancel")) {
85 this.cancel();
86 } else if (reason.hasChild("success")) {
87 this.finish();
88 }
89 } else {
90 Log.d("xmppService","remote terminated for no reason");
91 this.cancel();
92 }
93 } else if (packet.isAction("session-accept")) {
94 accept(packet);
95 } else if (packet.isAction("transport-info")) {
96 transportInfo(packet);
97 } else {
98 Log.d("xmppService","packet arrived in connection. action was "+packet.getAction());
99 }
100 }
101
102 public void init(Message message) {
103 this.message = message;
104 this.account = message.getConversation().getAccount();
105 this.initiator = this.account.getFullJid();
106 this.responder = this.message.getCounterpart();
107 this.sessionId = this.mJingleConnectionManager.nextRandomId();
108 if (this.candidates.size() > 0) {
109 this.sendInitRequest();
110 } else {
111 this.mJingleConnectionManager.getPrimaryCandidate(account, new OnPrimaryCandidateFound() {
112
113 @Override
114 public void onPrimaryCandidateFound(boolean success, final JingleCandidate candidate) {
115 if (success) {
116 final SocksConnection socksConnection = new SocksConnection(JingleConnection.this, candidate);
117 connections.put(candidate.getCid(), socksConnection);
118 socksConnection.connect(new OnSocksConnection() {
119
120 @Override
121 public void failed() {
122 sendInitRequest();
123 }
124
125 @Override
126 public void established() {
127 mergeCandidate(candidate);
128 sendInitRequest();
129 }
130 });
131 mergeCandidate(candidate);
132 } else {
133 sendInitRequest();
134 }
135 }
136 });
137 }
138
139 }
140
141 public void init(Account account, JinglePacket packet) {
142 this.status = STATUS_INITIATED;
143 Conversation conversation = this.mXmppConnectionService.findOrCreateConversation(account, packet.getFrom().split("/")[0], false);
144 this.message = new Message(conversation, "receiving image file", Message.ENCRYPTION_NONE);
145 this.message.setType(Message.TYPE_IMAGE);
146 this.message.setStatus(Message.STATUS_RECIEVING);
147 String[] fromParts = packet.getFrom().split("/");
148 this.message.setPresence(fromParts[1]);
149 this.account = account;
150 this.initiator = packet.getFrom();
151 this.responder = this.account.getFullJid();
152 this.sessionId = packet.getSessionId();
153 Content content = packet.getJingleContent();
154 this.transportId = content.getTransportId();
155 this.mergeCandidates(JingleCandidate.parse(content.getCanditates()));
156 this.fileOffer = packet.getJingleContent().getFileOffer();
157 if (fileOffer!=null) {
158 this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message);
159 Element fileSize = fileOffer.findChild("size");
160 Element fileName = fileOffer.findChild("name");
161 this.file.setExpectedSize(Long.parseLong(fileSize.getContent()));
162 conversation.getMessages().add(message);
163 this.mXmppConnectionService.databaseBackend.createMessage(message);
164 if (this.mXmppConnectionService.convChangedListener!=null) {
165 this.mXmppConnectionService.convChangedListener.onConversationListChanged();
166 }
167 if (this.file.getExpectedSize()<=this.mJingleConnectionManager.getAutoAcceptFileSize()) {
168 Log.d("xmppService","auto accepting file from "+packet.getFrom());
169 this.sendAccept();
170 } else {
171 Log.d("xmppService","not auto accepting new file offer with size: "+this.file.getExpectedSize()+" allowed size:"+this.mJingleConnectionManager.getAutoAcceptFileSize());
172 }
173 } else {
174 Log.d("xmppService","no file offer was attached. aborting");
175 }
176 }
177
178 private void sendInitRequest() {
179 JinglePacket packet = this.bootstrapPacket("session-initiate");
180 Content content = new Content();
181 if (message.getType() == Message.TYPE_IMAGE) {
182 content.setAttribute("creator", "initiator");
183 content.setAttribute("name", "a-file-offer");
184 this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message);
185 content.setFileOffer(this.file);
186 this.transportId = this.mJingleConnectionManager.nextRandomId();
187 content.setCandidates(this.transportId,getCandidatesAsElements());
188 packet.setContent(content);
189 Log.d("xmppService",packet.toString());
190 account.getXmppConnection().sendIqPacket(packet, this.responseListener);
191 this.status = STATUS_INITIATED;
192 }
193 }
194
195 private List<Element> getCandidatesAsElements() {
196 List<Element> elements = new ArrayList<Element>();
197 for(JingleCandidate c : this.candidates) {
198 elements.add(c.toElement());
199 }
200 return elements;
201 }
202
203 private void sendAccept() {
204 status = STATUS_ACCEPTED;
205 connectNextCandidate();
206 this.mJingleConnectionManager.getPrimaryCandidate(this.account, new OnPrimaryCandidateFound() {
207
208 @Override
209 public void onPrimaryCandidateFound(boolean success,final JingleCandidate candidate) {
210 final JinglePacket packet = bootstrapPacket("session-accept");
211 final Content content = new Content();
212 content.setFileOffer(fileOffer);
213 if ((success)&&(!equalCandidateExists(candidate))) {
214 final SocksConnection socksConnection = new SocksConnection(JingleConnection.this, candidate);
215 connections.put(candidate.getCid(), socksConnection);
216 socksConnection.connect(new OnSocksConnection() {
217
218 @Override
219 public void failed() {
220 content.setCandidates(transportId, getCandidatesAsElements());
221 packet.setContent(content);
222 account.getXmppConnection().sendIqPacket(packet,responseListener);
223 }
224
225 @Override
226 public void established() {
227 mergeCandidate(candidate);
228 content.setCandidates(transportId, getCandidatesAsElements());
229 packet.setContent(content);
230 account.getXmppConnection().sendIqPacket(packet,responseListener);
231 }
232 });
233 } else {
234 content.setCandidates(transportId, getCandidatesAsElements());
235 packet.setContent(content);
236 account.getXmppConnection().sendIqPacket(packet,responseListener);
237 }
238 }
239 });
240
241 }
242
243 private JinglePacket bootstrapPacket(String action) {
244 JinglePacket packet = new JinglePacket();
245 packet.setAction(action);
246 packet.setFrom(account.getFullJid());
247 packet.setTo(this.message.getCounterpart());
248 packet.setSessionId(this.sessionId);
249 packet.setInitiator(this.initiator);
250 return packet;
251 }
252
253 private void accept(JinglePacket packet) {
254 Log.d("xmppService","session-accept: "+packet.toString());
255 Content content = packet.getJingleContent();
256 mergeCandidates(JingleCandidate.parse(content.getCanditates()));
257 this.status = STATUS_ACCEPTED;
258 this.connectNextCandidate();
259 IqPacket response = packet.generateRespone(IqPacket.TYPE_RESULT);
260 account.getXmppConnection().sendIqPacket(response, null);
261 }
262
263 private void transportInfo(JinglePacket packet) {
264 Content content = packet.getJingleContent();
265 String cid = content.getUsedCandidate();
266 IqPacket response = packet.generateRespone(IqPacket.TYPE_RESULT);
267 if (cid!=null) {
268 Log.d("xmppService","candidate used by counterpart:"+cid);
269 JingleCandidate candidate = getCandidate(cid);
270 candidate.flagAsUsedByCounterpart();
271 this.receivedCandidate = true;
272 if ((status == STATUS_ACCEPTED)&&(this.sentCandidate)) {
273 this.connect();
274 } else {
275 Log.d("xmppService","ignoring because file is already in transmission or we havent sent our candidate yet");
276 }
277 } else if (content.hasCandidateError()) {
278 Log.d("xmppService","received candidate error");
279 this.receivedCandidate = true;
280 if (status == STATUS_ACCEPTED) {
281 this.connect();
282 }
283 }
284 account.getXmppConnection().sendIqPacket(response, null);
285 }
286
287 private void connect() {
288 final SocksConnection connection = chooseConnection();
289 this.status = STATUS_TRANSMITTING;
290 final OnFileTransmitted callback = new OnFileTransmitted() {
291
292 @Override
293 public void onFileTransmitted(JingleFile file) {
294 if (responder.equals(account.getFullJid())) {
295 sendSuccess();
296 mXmppConnectionService.markMessage(message, Message.STATUS_SEND);
297 }
298 Log.d("xmppService","sucessfully transmitted file. sha1:"+file.getSha1Sum());
299 }
300 };
301 if (connection.isProxy()&&(connection.getCandidate().isOurs())) {
302 Log.d("xmppService","candidate "+connection.getCandidate().getCid()+" was our proxy and needs activation");
303 IqPacket activation = new IqPacket(IqPacket.TYPE_SET);
304 activation.setTo(connection.getCandidate().getJid());
305 activation.query("http://jabber.org/protocol/bytestreams").setAttribute("sid", this.getSessionId());
306 activation.query().addChild("activate").setContent(this.getCounterPart());
307 this.account.getXmppConnection().sendIqPacket(activation, new OnIqPacketReceived() {
308
309 @Override
310 public void onIqPacketReceived(Account account, IqPacket packet) {
311 Log.d("xmppService","activation result: "+packet.toString());
312 if (initiator.equals(account.getFullJid())) {
313 Log.d("xmppService","we were initiating. sending file");
314 connection.send(file,callback);
315 } else {
316 connection.receive(file,callback);
317 Log.d("xmppService","we were responding. receiving file");
318 }
319 }
320 });
321 } else {
322 if (initiator.equals(account.getFullJid())) {
323 Log.d("xmppService","we were initiating. sending file");
324 connection.send(file,callback);
325 } else {
326 Log.d("xmppService","we were responding. receiving file");
327 connection.receive(file,callback);
328 }
329 }
330 }
331
332 private SocksConnection chooseConnection() {
333 Log.d("xmppService","choosing connection from "+this.connections.size()+" possibilties");
334 SocksConnection connection = null;
335 Iterator<Entry<String, SocksConnection>> it = this.connections.entrySet().iterator();
336 while (it.hasNext()) {
337 Entry<String, SocksConnection> pairs = it.next();
338 SocksConnection currentConnection = pairs.getValue();
339 Log.d("xmppService","comparing candidate: "+currentConnection.getCandidate().toString());
340 if (currentConnection.isEstablished()&&(currentConnection.getCandidate().isUsedByCounterpart()||(!currentConnection.getCandidate().isOurs()))) {
341 Log.d("xmppService","is usable");
342 if (connection==null) {
343 connection = currentConnection;
344 } else {
345 if (connection.getCandidate().getPriority()<currentConnection.getCandidate().getPriority()) {
346 connection = currentConnection;
347 } else if (connection.getCandidate().getPriority()==currentConnection.getCandidate().getPriority()) {
348 Log.d("xmppService","found two candidates with same priority");
349 if (initiator.equals(account.getFullJid())) {
350 if (currentConnection.getCandidate().isOurs()) {
351 connection = currentConnection;
352 }
353 } else {
354 if (!currentConnection.getCandidate().isOurs()) {
355 connection = currentConnection;
356 }
357 }
358 }
359 }
360 }
361 it.remove();
362 }
363 if (connection!=null) {
364 Log.d("xmppService","chose candidate: "+connection.getCandidate().getHost());
365 } else {
366 Log.d("xmppService","couldn't find candidate");
367 }
368 return connection;
369 }
370
371 private void sendSuccess() {
372 JinglePacket packet = bootstrapPacket("session-terminate");
373 Reason reason = new Reason();
374 reason.addChild("success");
375 packet.setReason(reason);
376 Log.d("xmppService","sending success. "+packet.toString());
377 this.account.getXmppConnection().sendIqPacket(packet, responseListener);
378 this.disconnect();
379 this.status = STATUS_FINISHED;
380 this.mXmppConnectionService.markMessage(this.message, Message.STATUS_RECIEVED);
381 }
382
383 private void finish() {
384 this.status = STATUS_FINISHED;
385 this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND);
386 this.disconnect();
387 }
388
389 public void cancel() {
390 this.disconnect();
391 this.status = STATUS_CANCELED;
392 this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND_REJECTED);
393 }
394
395 /*private void openOurCandidates() {
396 for(JingleCandidate candidate : this.candidates) {
397 if (candidate.isOurs()) {
398 final SocksConnection socksConnection = new SocksConnection(this,candidate);
399 connections.put(candidate.getCid(), socksConnection);
400 socksConnection.connect(new OnSocksConnection() {
401
402 @Override
403 public void failed() {
404 Log.d("xmppService","connection to our candidate failed");
405 }
406
407 @Override
408 public void established() {
409 Log.d("xmppService","connection to our candidate was successful");
410 }
411 });
412 }
413 }
414 }*/
415
416 private void connectNextCandidate() {
417 for(JingleCandidate candidate : this.candidates) {
418 if ((!connections.containsKey(candidate.getCid())&&(!candidate.isOurs()))) {
419 this.connectWithCandidate(candidate);
420 return;
421 }
422 }
423 this.sendCandidateError();
424 }
425
426 private void connectWithCandidate(final JingleCandidate candidate) {
427 final SocksConnection socksConnection = new SocksConnection(this,candidate);
428 connections.put(candidate.getCid(), socksConnection);
429 socksConnection.connect(new OnSocksConnection() {
430
431 @Override
432 public void failed() {
433 connectNextCandidate();
434 }
435
436 @Override
437 public void established() {
438 sendCandidateUsed(candidate.getCid());
439 if ((receivedCandidate)&&(status == STATUS_ACCEPTED)) {
440 connect();
441 }
442 }
443 });
444 }
445
446 private void disconnect() {
447 Iterator<Entry<String, SocksConnection>> it = this.connections.entrySet().iterator();
448 while (it.hasNext()) {
449 Entry<String, SocksConnection> pairs = it.next();
450 pairs.getValue().disconnect();
451 it.remove();
452 }
453 }
454
455 private void sendCandidateUsed(final String cid) {
456 JinglePacket packet = bootstrapPacket("transport-info");
457 Content content = new Content();
458 //TODO: put these into actual variables
459 content.setAttribute("creator", "initiator");
460 content.setAttribute("name", "a-file-offer");
461 content.setUsedCandidate(this.transportId, cid);
462 packet.setContent(content);
463 Log.d("xmppService","send using candidate: "+cid);
464 this.account.getXmppConnection().sendIqPacket(packet,responseListener);
465 this.sentCandidate = true;
466 }
467
468 private void sendCandidateError() {
469 JinglePacket packet = bootstrapPacket("transport-info");
470 Content content = new Content();
471 //TODO: put these into actual variables
472 content.setAttribute("creator", "initiator");
473 content.setAttribute("name", "a-file-offer");
474 content.setCandidateError(this.transportId);
475 packet.setContent(content);
476 Log.d("xmppService","send candidate error");
477 this.account.getXmppConnection().sendIqPacket(packet,responseListener);
478 this.sentCandidate = true;
479 }
480
481 public String getInitiator() {
482 return this.initiator;
483 }
484
485 public String getResponder() {
486 return this.responder;
487 }
488
489 public int getStatus() {
490 return this.status;
491 }
492
493 private boolean equalCandidateExists(JingleCandidate candidate) {
494 for(JingleCandidate c : this.candidates) {
495 if (c.equalValues(candidate)) {
496 return true;
497 }
498 }
499 return false;
500 }
501
502 private void mergeCandidate(JingleCandidate candidate) {
503 for(JingleCandidate c : this.candidates) {
504 if (c.equals(candidate)) {
505 return;
506 }
507 }
508 this.candidates.add(candidate);
509 }
510
511 private void mergeCandidates(List<JingleCandidate> candidates) {
512 for(JingleCandidate c : candidates) {
513 mergeCandidate(c);
514 }
515 }
516
517 private JingleCandidate getCandidate(String cid) {
518 for(JingleCandidate c : this.candidates) {
519 if (c.getCid().equals(cid)) {
520 return c;
521 }
522 }
523 return null;
524 }
525}