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