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