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