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