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