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 ibbBlockSize = 4096;
36
37 private int status = -1;
38 private Message message;
39 private String sessionId;
40 private Account account;
41 private String initiator;
42 private String responder;
43 private List<JingleCandidate> candidates = new ArrayList<JingleCandidate>();
44 private HashMap<String, JingleSocks5Transport> connections = new HashMap<String, JingleSocks5Transport>();
45
46 private String transportId;
47 private Element fileOffer;
48 private JingleFile file = null;
49
50 private boolean receivedCandidate = false;
51 private boolean sentCandidate = false;
52
53 private boolean acceptedAutomatically = false;
54
55 private JingleTransport transport = null;
56
57 private OnIqPacketReceived responseListener = new OnIqPacketReceived() {
58
59 @Override
60 public void onIqPacketReceived(Account account, IqPacket packet) {
61 if (packet.getType() == IqPacket.TYPE_ERROR) {
62 mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
63 status = STATUS_FAILED;
64 }
65 }
66 };
67
68 final OnFileTransmitted onFileTransmitted = new OnFileTransmitted() {
69
70 @Override
71 public void onFileTransmitted(JingleFile file) {
72 if (responder.equals(account.getFullJid())) {
73 sendSuccess();
74 if (acceptedAutomatically) {
75 message.markUnread();
76 }
77 mXmppConnectionService.markMessage(message, Message.STATUS_RECIEVED);
78 }
79 mXmppConnectionService.databaseBackend.createMessage(message);
80 Log.d("xmppService","sucessfully transmitted file. sha1:"+file.getSha1Sum());
81 }
82 };
83
84 private OnProxyActivated onProxyActivated = new OnProxyActivated() {
85
86 @Override
87 public void success() {
88 if (initiator.equals(account.getFullJid())) {
89 Log.d("xmppService","we were initiating. sending file");
90 transport.send(file,onFileTransmitted);
91 } else {
92 transport.receive(file,onFileTransmitted);
93 Log.d("xmppService","we were responding. receiving file");
94 }
95 }
96
97 @Override
98 public void failed() {
99 Log.d("xmppService","proxy activation failed");
100 }
101 };
102
103 public JingleConnection(JingleConnectionManager mJingleConnectionManager) {
104 this.mJingleConnectionManager = mJingleConnectionManager;
105 this.mXmppConnectionService = mJingleConnectionManager.getXmppConnectionService();
106 }
107
108 public String getSessionId() {
109 return this.sessionId;
110 }
111
112 public String getAccountJid() {
113 return this.account.getFullJid();
114 }
115
116 public String getCounterPart() {
117 return this.message.getCounterpart();
118 }
119
120 public void deliverPacket(JinglePacket packet) {
121
122 if (packet.isAction("session-terminate")) {
123 Reason reason = packet.getReason();
124 if (reason!=null) {
125 if (reason.hasChild("cancel")) {
126 this.cancel();
127 } else if (reason.hasChild("success")) {
128 this.finish();
129 }
130 } else {
131 Log.d("xmppService","remote terminated for no reason");
132 this.cancel();
133 }
134 } else if (packet.isAction("session-accept")) {
135 receiveAccept(packet);
136 } else if (packet.isAction("transport-info")) {
137 receiveTransportInfo(packet);
138 } else if (packet.isAction("transport-replace")) {
139 if (packet.getJingleContent().hasIbbTransport()) {
140 this.receiveFallbackToIbb(packet);
141 } else {
142 Log.d("xmppService","trying to fallback to something unknown"+packet.toString());
143 }
144 } else if (packet.isAction("transport-accept")) {
145 this.receiveTransportAccept(packet);
146 } else {
147 Log.d("xmppService","packet arrived in connection. action was "+packet.getAction());
148 }
149 }
150
151 public void init(Message message) {
152 this.message = message;
153 this.account = message.getConversation().getAccount();
154 this.initiator = this.account.getFullJid();
155 this.responder = this.message.getCounterpart();
156 this.sessionId = this.mJingleConnectionManager.nextRandomId();
157 if (this.candidates.size() > 0) {
158 this.sendInitRequest();
159 } else {
160 this.mJingleConnectionManager.getPrimaryCandidate(account, new OnPrimaryCandidateFound() {
161
162 @Override
163 public void onPrimaryCandidateFound(boolean success, final JingleCandidate candidate) {
164 if (success) {
165 final JingleSocks5Transport socksConnection = new JingleSocks5Transport(JingleConnection.this, candidate);
166 connections.put(candidate.getCid(), socksConnection);
167 socksConnection.connect(new OnTransportConnected() {
168
169 @Override
170 public void failed() {
171 Log.d("xmppService","connection to our own primary candidete failed");
172 sendInitRequest();
173 }
174
175 @Override
176 public void established() {
177 Log.d("xmppService","succesfully connected to our own primary candidate");
178 mergeCandidate(candidate);
179 sendInitRequest();
180 }
181 });
182 mergeCandidate(candidate);
183 } else {
184 Log.d("xmppService","no primary candidate of our own was found");
185 sendInitRequest();
186 }
187 }
188 });
189 }
190
191 }
192
193 public void init(Account account, JinglePacket packet) {
194 this.status = STATUS_INITIATED;
195 Conversation conversation = this.mXmppConnectionService.findOrCreateConversation(account, packet.getFrom().split("/")[0], false);
196 this.message = new Message(conversation, "receiving image file", Message.ENCRYPTION_NONE);
197 this.message.setType(Message.TYPE_IMAGE);
198 this.message.setStatus(Message.STATUS_RECEIVED_OFFER);
199 this.message.setJingleConnection(this);
200 String[] fromParts = packet.getFrom().split("/");
201 this.message.setPresence(fromParts[1]);
202 this.account = account;
203 this.initiator = packet.getFrom();
204 this.responder = this.account.getFullJid();
205 this.sessionId = packet.getSessionId();
206 Content content = packet.getJingleContent();
207 this.transportId = content.getTransportId();
208 this.mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren()));
209 this.fileOffer = packet.getJingleContent().getFileOffer();
210 if (fileOffer!=null) {
211 this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message);
212 Element fileSize = fileOffer.findChild("size");
213 Element fileName = fileOffer.findChild("name");
214 this.file.setExpectedSize(Long.parseLong(fileSize.getContent()));
215 conversation.getMessages().add(message);
216 if (this.file.getExpectedSize()<=this.mJingleConnectionManager.getAutoAcceptFileSize()) {
217 Log.d("xmppService","auto accepting file from "+packet.getFrom());
218 this.acceptedAutomatically = true;
219 this.sendAccept();
220 } else {
221 message.markUnread();
222 Log.d("xmppService","not auto accepting new file offer with size: "+this.file.getExpectedSize()+" allowed size:"+this.mJingleConnectionManager.getAutoAcceptFileSize());
223 if (this.mXmppConnectionService.convChangedListener!=null) {
224 this.mXmppConnectionService.convChangedListener.onConversationListChanged();
225 }
226 }
227 } else {
228 Log.d("xmppService","no file offer was attached. aborting");
229 }
230 }
231
232 private void sendInitRequest() {
233 JinglePacket packet = this.bootstrapPacket("session-initiate");
234 Content content = new Content();
235 if (message.getType() == Message.TYPE_IMAGE) {
236 content.setAttribute("creator", "initiator");
237 content.setAttribute("name", "a-file-offer");
238 content.setTransportId(this.transportId);
239 this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message);
240 content.setFileOffer(this.file);
241 this.transportId = this.mJingleConnectionManager.nextRandomId();
242 content.setTransportId(this.transportId);
243 content.socks5transport().setChildren(getCandidatesAsElements());
244 packet.setContent(content);
245 this.sendJinglePacket(packet);
246 this.status = STATUS_INITIATED;
247 }
248 }
249
250 private List<Element> getCandidatesAsElements() {
251 List<Element> elements = new ArrayList<Element>();
252 for(JingleCandidate c : this.candidates) {
253 elements.add(c.toElement());
254 }
255 return elements;
256 }
257
258 private void sendAccept() {
259 status = STATUS_ACCEPTED;
260 mXmppConnectionService.markMessage(message, Message.STATUS_RECIEVING);
261 this.mJingleConnectionManager.getPrimaryCandidate(this.account, new OnPrimaryCandidateFound() {
262
263 @Override
264 public void onPrimaryCandidateFound(boolean success,final JingleCandidate candidate) {
265 final JinglePacket packet = bootstrapPacket("session-accept");
266 final Content content = new Content();
267 content.setFileOffer(fileOffer);
268 content.setTransportId(transportId);
269 if ((success)&&(!equalCandidateExists(candidate))) {
270 final JingleSocks5Transport socksConnection = new JingleSocks5Transport(JingleConnection.this, candidate);
271 connections.put(candidate.getCid(), socksConnection);
272 socksConnection.connect(new OnTransportConnected() {
273
274 @Override
275 public void failed() {
276 Log.d("xmppService","connection to our own primary candidate failed");
277 content.socks5transport().setChildren(getCandidatesAsElements());
278 packet.setContent(content);
279 sendJinglePacket(packet);
280 connectNextCandidate();
281 }
282
283 @Override
284 public void established() {
285 Log.d("xmppService","connected to primary candidate");
286 mergeCandidate(candidate);
287 content.socks5transport().setChildren(getCandidatesAsElements());
288 packet.setContent(content);
289 sendJinglePacket(packet);
290 connectNextCandidate();
291 }
292 });
293 } else {
294 Log.d("xmppService","did not find a primary candidate for ourself");
295 content.socks5transport().setChildren(getCandidatesAsElements());
296 packet.setContent(content);
297 sendJinglePacket(packet);
298 connectNextCandidate();
299 }
300 }
301 });
302
303 }
304
305 private JinglePacket bootstrapPacket(String action) {
306 JinglePacket packet = new JinglePacket();
307 packet.setAction(action);
308 packet.setFrom(account.getFullJid());
309 packet.setTo(this.message.getCounterpart());
310 packet.setSessionId(this.sessionId);
311 packet.setInitiator(this.initiator);
312 return packet;
313 }
314
315 private void sendJinglePacket(JinglePacket packet) {
316 //Log.d("xmppService",packet.toString());
317 account.getXmppConnection().sendIqPacket(packet,responseListener);
318 }
319
320 private void receiveAccept(JinglePacket packet) {
321 Content content = packet.getJingleContent();
322 mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren()));
323 this.status = STATUS_ACCEPTED;
324 mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
325 this.connectNextCandidate();
326 IqPacket response = packet.generateRespone(IqPacket.TYPE_RESULT);
327 account.getXmppConnection().sendIqPacket(response, null);
328 }
329
330 private void receiveTransportInfo(JinglePacket packet) {
331 Content content = packet.getJingleContent();
332 if (content.hasSocks5Transport()) {
333 if (content.socks5transport().hasChild("activated")) {
334 onProxyActivated.success();
335 } else if (content.socks5transport().hasChild("activated")) {
336 onProxyActivated.failed();
337 } else if (content.socks5transport().hasChild("candidate-error")) {
338 Log.d("xmppService","received candidate error");
339 this.receivedCandidate = true;
340 if (status == STATUS_ACCEPTED) {
341 this.connect();
342 }
343 } else if (content.socks5transport().hasChild("candidate-used")){
344 String cid = content.socks5transport().findChild("candidate-used").getAttribute("cid");
345 if (cid!=null) {
346 Log.d("xmppService","candidate used by counterpart:"+cid);
347 JingleCandidate candidate = getCandidate(cid);
348 candidate.flagAsUsedByCounterpart();
349 this.receivedCandidate = true;
350 if ((status == STATUS_ACCEPTED)&&(this.sentCandidate)) {
351 this.connect();
352 } else {
353 Log.d("xmppService","ignoring because file is already in transmission or we havent sent our candidate yet");
354 }
355 } else {
356 Log.d("xmppService","couldn't read used candidate");
357 }
358 } else {
359 Log.d("xmppService","empty transport");
360 }
361 }
362
363 IqPacket response = packet.generateRespone(IqPacket.TYPE_RESULT);
364 account.getXmppConnection().sendIqPacket(response, null);
365 }
366
367 private void connect() {
368 final JingleSocks5Transport connection = chooseConnection();
369 this.transport = connection;
370 if (connection==null) {
371 Log.d("xmppService","could not find suitable candidate");
372 this.disconnect();
373 if (this.initiator.equals(account.getFullJid())) {
374 this.sendFallbackToIbb();
375 }
376 } else {
377 this.status = STATUS_TRANSMITTING;
378 if (connection.isProxy()) {
379 if (connection.getCandidate().isOurs()) {
380 Log.d("xmppService","candidate "+connection.getCandidate().getCid()+" was our proxy and needs activation");
381 IqPacket activation = new IqPacket(IqPacket.TYPE_SET);
382 activation.setTo(connection.getCandidate().getJid());
383 activation.query("http://jabber.org/protocol/bytestreams").setAttribute("sid", this.getSessionId());
384 activation.query().addChild("activate").setContent(this.getCounterPart());
385 this.account.getXmppConnection().sendIqPacket(activation, new OnIqPacketReceived() {
386
387 @Override
388 public void onIqPacketReceived(Account account, IqPacket packet) {
389 if (packet.getType()==IqPacket.TYPE_ERROR) {
390 onProxyActivated.failed();
391 } else {
392 onProxyActivated.success();
393 sendProxyActivated(connection.getCandidate().getCid());
394 }
395 }
396 });
397 }
398 } else {
399 if (initiator.equals(account.getFullJid())) {
400 Log.d("xmppService","we were initiating. sending file");
401 connection.send(file,onFileTransmitted);
402 } else {
403 Log.d("xmppService","we were responding. receiving file");
404 connection.receive(file,onFileTransmitted);
405 }
406 }
407 }
408 }
409
410 private JingleSocks5Transport chooseConnection() {
411 JingleSocks5Transport connection = null;
412 Iterator<Entry<String, JingleSocks5Transport>> it = this.connections.entrySet().iterator();
413 while (it.hasNext()) {
414 Entry<String, JingleSocks5Transport> pairs = it.next();
415 JingleSocks5Transport currentConnection = pairs.getValue();
416 //Log.d("xmppService","comparing candidate: "+currentConnection.getCandidate().toString());
417 if (currentConnection.isEstablished()&&(currentConnection.getCandidate().isUsedByCounterpart()||(!currentConnection.getCandidate().isOurs()))) {
418 //Log.d("xmppService","is usable");
419 if (connection==null) {
420 connection = currentConnection;
421 } else {
422 if (connection.getCandidate().getPriority()<currentConnection.getCandidate().getPriority()) {
423 connection = currentConnection;
424 } else if (connection.getCandidate().getPriority()==currentConnection.getCandidate().getPriority()) {
425 //Log.d("xmppService","found two candidates with same priority");
426 if (initiator.equals(account.getFullJid())) {
427 if (currentConnection.getCandidate().isOurs()) {
428 connection = currentConnection;
429 }
430 } else {
431 if (!currentConnection.getCandidate().isOurs()) {
432 connection = currentConnection;
433 }
434 }
435 }
436 }
437 }
438 it.remove();
439 }
440 return connection;
441 }
442
443 private void sendSuccess() {
444 JinglePacket packet = bootstrapPacket("session-terminate");
445 Reason reason = new Reason();
446 reason.addChild("success");
447 packet.setReason(reason);
448 this.sendJinglePacket(packet);
449 this.disconnect();
450 this.status = STATUS_FINISHED;
451 this.mXmppConnectionService.markMessage(this.message, Message.STATUS_RECIEVED);
452 }
453
454 private void sendFallbackToIbb() {
455 JinglePacket packet = this.bootstrapPacket("transport-replace");
456 Content content = new Content("initiator","a-file-offer");
457 this.transportId = this.mJingleConnectionManager.nextRandomId();
458 content.setTransportId(this.transportId);
459 content.ibbTransport().setAttribute("block-size",""+this.ibbBlockSize);
460 packet.setContent(content);
461 this.sendJinglePacket(packet);
462 }
463
464 private void receiveFallbackToIbb(JinglePacket packet) {
465 String receivedBlockSize = packet.getJingleContent().ibbTransport().getAttribute("block-size");
466 if (receivedBlockSize!=null) {
467 int bs = Integer.parseInt(receivedBlockSize);
468 if (bs>this.ibbBlockSize) {
469 this.ibbBlockSize = bs;
470 }
471 }
472 this.transportId = packet.getJingleContent().getTransportId();
473 this.transport = new JingleInbandTransport(this.account,this.responder,this.transportId,this.ibbBlockSize);
474 this.transport.receive(file, onFileTransmitted);
475 JinglePacket answer = bootstrapPacket("transport-accept");
476 Content content = new Content("initiator", "a-file-offer");
477 content.setTransportId(this.transportId);
478 content.ibbTransport().setAttribute("block-size", ""+this.ibbBlockSize);
479 answer.setContent(content);
480 this.sendJinglePacket(answer);
481 }
482
483 private void receiveTransportAccept(JinglePacket packet) {
484 if (packet.getJingleContent().hasIbbTransport()) {
485 String receivedBlockSize = packet.getJingleContent().ibbTransport().getAttribute("block-size");
486 if (receivedBlockSize!=null) {
487 int bs = Integer.parseInt(receivedBlockSize);
488 if (bs>this.ibbBlockSize) {
489 this.ibbBlockSize = bs;
490 }
491 }
492 this.transport = new JingleInbandTransport(this.account,this.responder,this.transportId,this.ibbBlockSize);
493 this.transport.connect(new OnTransportConnected() {
494
495 @Override
496 public void failed() {
497 Log.d("xmppService","ibb open failed");
498 }
499
500 @Override
501 public void established() {
502 JingleConnection.this.transport.send(file, onFileTransmitted);
503 }
504 });
505 } else {
506 Log.d("xmppService","invalid transport accept");
507 }
508 }
509
510 private void finish() {
511 this.status = STATUS_FINISHED;
512 this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND);
513 this.disconnect();
514 }
515
516 public void cancel() {
517 this.disconnect();
518 this.status = STATUS_CANCELED;
519 this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND_REJECTED);
520 }
521
522 private void connectNextCandidate() {
523 for(JingleCandidate candidate : this.candidates) {
524 if ((!connections.containsKey(candidate.getCid())&&(!candidate.isOurs()))) {
525 this.connectWithCandidate(candidate);
526 return;
527 }
528 }
529 this.sendCandidateError();
530 }
531
532 private void connectWithCandidate(final JingleCandidate candidate) {
533 final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this,candidate);
534 connections.put(candidate.getCid(), socksConnection);
535 socksConnection.connect(new OnTransportConnected() {
536
537 @Override
538 public void failed() {
539 Log.d("xmppService", "connection failed with "+candidate.getHost()+":"+candidate.getPort());
540 connectNextCandidate();
541 }
542
543 @Override
544 public void established() {
545 Log.d("xmppService", "established connection with "+candidate.getHost()+":"+candidate.getPort());
546 sendCandidateUsed(candidate.getCid());
547 }
548 });
549 }
550
551 private void disconnect() {
552 Iterator<Entry<String, JingleSocks5Transport>> it = this.connections.entrySet().iterator();
553 while (it.hasNext()) {
554 Entry<String, JingleSocks5Transport> pairs = it.next();
555 pairs.getValue().disconnect();
556 it.remove();
557 }
558 }
559
560 private void sendProxyActivated(String cid) {
561 JinglePacket packet = bootstrapPacket("transport-info");
562 Content content = new Content("inititaor","a-file-offer");
563 content.setTransportId(this.transportId);
564 content.socks5transport().addChild("activated").setAttribute("cid", cid);
565 packet.setContent(content);
566 this.sendJinglePacket(packet);
567 }
568
569 private void sendCandidateUsed(final String cid) {
570 JinglePacket packet = bootstrapPacket("transport-info");
571 Content content = new Content("initiator","a-file-offer");
572 content.setTransportId(this.transportId);
573 content.socks5transport().addChild("candidate-used").setAttribute("cid", cid);
574 packet.setContent(content);
575 this.sentCandidate = true;
576 if ((receivedCandidate)&&(status == STATUS_ACCEPTED)) {
577 connect();
578 }
579 this.sendJinglePacket(packet);
580 }
581
582 private void sendCandidateError() {
583 JinglePacket packet = bootstrapPacket("transport-info");
584 Content content = new Content("initiator","a-file-offer");
585 content.setTransportId(this.transportId);
586 content.socks5transport().addChild("candidate-error");
587 packet.setContent(content);
588 this.sentCandidate = true;
589 if ((receivedCandidate)&&(status == STATUS_ACCEPTED)) {
590 connect();
591 }
592 this.sendJinglePacket(packet);
593 }
594
595 public String getInitiator() {
596 return this.initiator;
597 }
598
599 public String getResponder() {
600 return this.responder;
601 }
602
603 public int getStatus() {
604 return this.status;
605 }
606
607 private boolean equalCandidateExists(JingleCandidate candidate) {
608 for(JingleCandidate c : this.candidates) {
609 if (c.equalValues(candidate)) {
610 return true;
611 }
612 }
613 return false;
614 }
615
616 private void mergeCandidate(JingleCandidate candidate) {
617 for(JingleCandidate c : this.candidates) {
618 if (c.equals(candidate)) {
619 return;
620 }
621 }
622 this.candidates.add(candidate);
623 }
624
625 private void mergeCandidates(List<JingleCandidate> candidates) {
626 for(JingleCandidate c : candidates) {
627 mergeCandidate(c);
628 }
629 }
630
631 private JingleCandidate getCandidate(String cid) {
632 for(JingleCandidate c : this.candidates) {
633 if (c.getCid().equals(cid)) {
634 return c;
635 }
636 }
637 return null;
638 }
639
640 interface OnProxyActivated {
641 public void success();
642 public void failed();
643 }
644
645 public boolean hasTransportId(String sid) {
646 return sid.equals(this.transportId);
647 }
648
649 public JingleTransport getTransport() {
650 return this.transport;
651 }
652
653 public void accept() {
654 if (status==STATUS_INITIATED) {
655 new Thread(new Runnable() {
656
657 @Override
658 public void run() {
659 sendAccept();
660 }
661 }).start();
662 } else {
663 Log.d("xmppService","status ("+status+") was not ok");
664 }
665 }
666}