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