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.getResponder());
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 final SocksConnection socksConnection = new SocksConnection(this,candidate);
326 connections.put(socksConnection.getCid(), socksConnection);
327 socksConnection.connect(new OnSocksConnection() {
328
329 @Override
330 public void failed() {
331 connectNextCandidate();
332 }
333
334 @Override
335 public void established() {
336 if (candidatesUsedByCounterpart.contains(socksConnection.getCid())) {
337 if (status==STATUS_ACCEPTED) {
338 connect(socksConnection);
339 } else {
340 Log.d("xmppService","ignoring cuz already transmitting");
341 }
342 } else {
343 sendCandidateUsed(socksConnection.getCid());
344 }
345 }
346 });
347 }
348
349 private void disconnect() {
350 Iterator<Entry<String, SocksConnection>> it = this.connections.entrySet().iterator();
351 while (it.hasNext()) {
352 Entry<String, SocksConnection> pairs = it.next();
353 pairs.getValue().disconnect();
354 it.remove();
355 }
356 }
357
358 private void sendCandidateUsed(final String cid) {
359 JinglePacket packet = bootstrapPacket("transport-info");
360 Content content = new Content();
361 content.setUsedCandidate(this.content.getTransportId(), cid);
362 packet.setContent(content);
363 Log.d("xmppService","send using candidate: "+cid);
364 this.account.getXmppConnection().sendIqPacket(packet, new OnIqPacketReceived() {
365
366 @Override
367 public void onIqPacketReceived(Account account, IqPacket packet) {
368 Log.d("xmppService","got ack for our candidate used");
369 if (status==STATUS_ACCEPTED) {
370 connect(connections.get(cid));
371 } else {
372 Log.d("xmppService","ignoring cuz already transmitting");
373 }
374 }
375 });
376 }
377
378 public String getInitiator() {
379 return this.initiator;
380 }
381
382 public String getResponder() {
383 return this.responder;
384 }
385
386 public int getStatus() {
387 return this.status;
388 }
389
390 private boolean equalCandidateExists(Element candidate) {
391 for(Element c : this.candidates) {
392 if (c.getAttribute("host").equals(candidate.getAttribute("host"))&&(c.getAttribute("port").equals(candidate.getAttribute("port")))) {
393 return true;
394 }
395 }
396 return false;
397 }
398
399 private void mergeCandidate(Element candidate) {
400 for(Element c : this.candidates) {
401 if (c.getAttribute("cid").equals(candidate.getAttribute("cid"))) {
402 return;
403 }
404 }
405 this.candidates.add(candidate);
406 }
407
408 private void mergeCandidates(List<Element> candidates) {
409 for(Element c : candidates) {
410 mergeCandidate(c);
411 }
412 }
413
414 private Element getCandidate(String cid) {
415 for(Element c : this.candidates) {
416 if (c.getAttribute("cid").equals(cid)) {
417 return c;
418 }
419 }
420 return null;
421 }
422}