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