1package eu.siacs.conversations.xmpp.jingle;
2
3import android.util.Base64;
4import android.util.Log;
5
6import com.google.common.base.Objects;
7import com.google.common.base.Preconditions;
8
9import java.lang.ref.WeakReference;
10import java.security.SecureRandom;
11import java.util.HashMap;
12import java.util.Map;
13import java.util.concurrent.ConcurrentHashMap;
14
15import eu.siacs.conversations.Config;
16import eu.siacs.conversations.entities.Account;
17import eu.siacs.conversations.entities.Conversation;
18import eu.siacs.conversations.entities.Conversational;
19import eu.siacs.conversations.entities.Message;
20import eu.siacs.conversations.entities.Transferable;
21import eu.siacs.conversations.services.AbstractConnectionManager;
22import eu.siacs.conversations.services.XmppConnectionService;
23import eu.siacs.conversations.xml.Element;
24import eu.siacs.conversations.xml.Namespace;
25import eu.siacs.conversations.xmpp.OnIqPacketReceived;
26import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
27import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
28import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
29import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
30import eu.siacs.conversations.xmpp.stanzas.IqPacket;
31import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
32import rocks.xmpp.addr.Jid;
33
34public class JingleConnectionManager extends AbstractConnectionManager {
35 private final HashMap<RtpSessionProposal, DeviceDiscoveryState> rtpSessionProposals = new HashMap<>();
36 private final Map<AbstractJingleConnection.Id, AbstractJingleConnection> connections = new ConcurrentHashMap<>();
37
38 private HashMap<Jid, JingleCandidate> primaryCandidates = new HashMap<>();
39
40 public JingleConnectionManager(XmppConnectionService service) {
41 super(service);
42 }
43
44 static String nextRandomId() {
45 final byte[] id = new byte[16];
46 new SecureRandom().nextBytes(id);
47 return Base64.encodeToString(id, Base64.NO_WRAP | Base64.NO_PADDING);
48 }
49
50 public void deliverPacket(final Account account, final JinglePacket packet) {
51 final String sessionId = packet.getSessionId();
52 if (sessionId == null) {
53 respondWithJingleError(account, packet, "unknown-session", "item-not-found", "cancel");
54 return;
55 }
56 final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, packet);
57 final AbstractJingleConnection existingJingleConnection = connections.get(id);
58 if (existingJingleConnection != null) {
59 existingJingleConnection.deliverPacket(packet);
60 } else if (packet.getAction() == JinglePacket.Action.SESSION_INITIATE) {
61 final Jid from = packet.getFrom();
62 final Content content = packet.getJingleContent();
63 final String descriptionNamespace = content == null ? null : content.getDescriptionNamespace();
64 final AbstractJingleConnection connection;
65 if (FileTransferDescription.NAMESPACES.contains(descriptionNamespace)) {
66 connection = new JingleFileTransferConnection(this, id, from);
67 } else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace) && !usesTor(account)) {
68 if (isBusy()) {
69 mXmppConnectionService.sendIqPacket(account, packet.generateResponse(IqPacket.TYPE.RESULT), null);
70 final JinglePacket sessionTermination = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
71 sessionTermination.setTo(id.with);
72 sessionTermination.setReason(Reason.BUSY, null);
73 mXmppConnectionService.sendIqPacket(account, sessionTermination, null);
74 return;
75 }
76 connection = new JingleRtpConnection(this, id, from);
77 } else {
78 respondWithJingleError(account, packet, "unsupported-info", "feature-not-implemented", "cancel");
79 return;
80 }
81 connections.put(id, connection);
82 connection.deliverPacket(packet);
83 } else {
84 Log.d(Config.LOGTAG, "unable to route jingle packet: " + packet);
85 respondWithJingleError(account, packet, "unknown-session", "item-not-found", "cancel");
86 }
87 }
88
89 private boolean usesTor(final Account account) {
90 return account.isOnion() || mXmppConnectionService.useTorToConnect();
91 }
92
93 private boolean isBusy() {
94 for (AbstractJingleConnection connection : this.connections.values()) {
95 if (connection instanceof JingleRtpConnection) {
96 return true;
97 }
98 }
99 synchronized (this.rtpSessionProposals) {
100 return this.rtpSessionProposals.containsValue(DeviceDiscoveryState.DISCOVERED) || this.rtpSessionProposals.containsValue(DeviceDiscoveryState.SEARCHING);
101 }
102 }
103
104 public void respondWithJingleError(final Account account, final IqPacket original, String jingleCondition, String condition, String conditionType) {
105 final IqPacket response = original.generateResponse(IqPacket.TYPE.ERROR);
106 final Element error = response.addChild("error");
107 error.setAttribute("type", conditionType);
108 error.addChild(condition, "urn:ietf:params:xml:ns:xmpp-stanzas");
109 error.addChild(jingleCondition, "urn:xmpp:jingle:errors:1");
110 account.getXmppConnection().sendIqPacket(response, null);
111 }
112
113 public void deliverMessage(final Account account, final Jid to, final Jid from, final Element message, String serverMsgId, long timestamp) {
114 Preconditions.checkArgument(Namespace.JINGLE_MESSAGE.equals(message.getNamespace()));
115 final String sessionId = message.getAttribute("id");
116 if (sessionId == null) {
117 return;
118 }
119 if ("accept".equals(message.getName())) {
120 for (AbstractJingleConnection connection : connections.values()) {
121 if (connection instanceof JingleRtpConnection) {
122 final JingleRtpConnection rtpConnection = (JingleRtpConnection) connection;
123 final AbstractJingleConnection.Id id = connection.getId();
124 if (id.account == account && id.sessionId.equals(sessionId)) {
125 rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
126 return;
127 }
128 }
129 }
130 return;
131 }
132 final boolean addressedToSelf = from.asBareJid().equals(account.getJid().asBareJid());
133 final AbstractJingleConnection.Id id;
134 if (addressedToSelf) {
135 if (to.isFullJid()) {
136 id = AbstractJingleConnection.Id.of(account, to, sessionId);
137 } else {
138 return;
139 }
140 } else {
141 id = AbstractJingleConnection.Id.of(account, from, sessionId);
142 }
143 final AbstractJingleConnection existingJingleConnection = connections.get(id);
144 if (existingJingleConnection != null) {
145 if (existingJingleConnection instanceof JingleRtpConnection) {
146 ((JingleRtpConnection) existingJingleConnection).deliveryMessage(from, message, serverMsgId, timestamp);
147 } else {
148 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + existingJingleConnection.getClass().getName() + " does not support jingle messages");
149 }
150 return;
151 }
152
153 if (addressedToSelf) {
154 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignore jingle message from self");
155 }
156
157 if ("propose".equals(message.getName())) {
158 final Element description = message.findChild("description");
159 final String namespace = description == null ? null : description.getNamespace();
160 if (Namespace.JINGLE_APPS_RTP.equals(namespace) && !usesTor(account)) {
161 if (isBusy()) {
162 final MessagePacket reject = mXmppConnectionService.getMessageGenerator().sessionReject(from, sessionId);
163 mXmppConnectionService.sendMessagePacket(account, reject);
164 } else {
165 final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, from);
166 this.connections.put(id, rtpConnection);
167 rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
168 }
169 } else {
170 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to react to proposed " + namespace + " session");
171 }
172 } else if ("proceed".equals(message.getName())) {
173
174 final RtpSessionProposal proposal = new RtpSessionProposal(account, from.asBareJid(), sessionId);
175 synchronized (rtpSessionProposals) {
176 if (rtpSessionProposals.remove(proposal) != null) {
177 final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, account.getJid());
178 this.connections.put(id, rtpConnection);
179 rtpConnection.transitionOrThrow(AbstractJingleConnection.State.PROPOSED);
180 rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
181 } else {
182 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + from + " to deliver proceed");
183 }
184 }
185 } else if ("reject".equals(message.getName())) {
186 final RtpSessionProposal proposal = new RtpSessionProposal(account, from.asBareJid(), sessionId);
187 synchronized (rtpSessionProposals) {
188 if (rtpSessionProposals.remove(proposal) != null) {
189 writeLogMissedOutgoing(account, proposal.with, proposal.sessionId, serverMsgId, timestamp);
190 mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, proposal.with, proposal.sessionId, RtpEndUserState.DECLINED_OR_BUSY);
191 } else {
192 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + from + " to deliver reject");
193 }
194 }
195 } else {
196 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retrieved out of order jingle message");
197 }
198
199 }
200
201 private void writeLogMissedOutgoing(final Account account, Jid with, final String sessionId, String serverMsgId, long timestamp) {
202 final Conversation conversation = mXmppConnectionService.findOrCreateConversation(
203 account,
204 with.asBareJid(),
205 false,
206 false
207 );
208 final Message message = new Message(
209 conversation,
210 Message.STATUS_SEND,
211 Message.TYPE_RTP_SESSION,
212 sessionId
213 );
214 message.setServerMsgId(serverMsgId);
215 message.setTime(timestamp);
216 writeMessage(message);
217 }
218
219 private void writeMessage(final Message message) {
220 final Conversational conversational = message.getConversation();
221 if (conversational instanceof Conversation) {
222 ((Conversation) conversational).add(message);
223 mXmppConnectionService.databaseBackend.createMessage(message);
224 mXmppConnectionService.updateConversationUi();
225 } else {
226 throw new IllegalStateException("Somehow the conversation in a message was a stub");
227 }
228 }
229
230 public void startJingleFileTransfer(final Message message) {
231 Preconditions.checkArgument(message.isFileOrImage(), "Message is not of type file or image");
232 final Transferable old = message.getTransferable();
233 if (old != null) {
234 old.cancel();
235 }
236 final Account account = message.getConversation().getAccount();
237 final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(message);
238 final JingleFileTransferConnection connection = new JingleFileTransferConnection(this, id, account.getJid());
239 mXmppConnectionService.markMessage(message, Message.STATUS_WAITING);
240 this.connections.put(id, connection);
241 connection.init(message);
242 }
243
244 void finishConnection(final AbstractJingleConnection connection) {
245 this.connections.remove(connection.getId());
246 }
247
248 void getPrimaryCandidate(final Account account, final boolean initiator, final OnPrimaryCandidateFound listener) {
249 if (Config.DISABLE_PROXY_LOOKUP) {
250 listener.onPrimaryCandidateFound(false, null);
251 return;
252 }
253 if (!this.primaryCandidates.containsKey(account.getJid().asBareJid())) {
254 final Jid proxy = account.getXmppConnection().findDiscoItemByFeature(Namespace.BYTE_STREAMS);
255 if (proxy != null) {
256 IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
257 iq.setTo(proxy);
258 iq.query(Namespace.BYTE_STREAMS);
259 account.getXmppConnection().sendIqPacket(iq, new OnIqPacketReceived() {
260
261 @Override
262 public void onIqPacketReceived(Account account, IqPacket packet) {
263 final Element streamhost = packet.query().findChild("streamhost", Namespace.BYTE_STREAMS);
264 final String host = streamhost == null ? null : streamhost.getAttribute("host");
265 final String port = streamhost == null ? null : streamhost.getAttribute("port");
266 if (host != null && port != null) {
267 try {
268 JingleCandidate candidate = new JingleCandidate(nextRandomId(), true);
269 candidate.setHost(host);
270 candidate.setPort(Integer.parseInt(port));
271 candidate.setType(JingleCandidate.TYPE_PROXY);
272 candidate.setJid(proxy);
273 candidate.setPriority(655360 + (initiator ? 30 : 0));
274 primaryCandidates.put(account.getJid().asBareJid(), candidate);
275 listener.onPrimaryCandidateFound(true, candidate);
276 } catch (final NumberFormatException e) {
277 listener.onPrimaryCandidateFound(false, null);
278 }
279 } else {
280 listener.onPrimaryCandidateFound(false, null);
281 }
282 }
283 });
284 } else {
285 listener.onPrimaryCandidateFound(false, null);
286 }
287
288 } else {
289 listener.onPrimaryCandidateFound(true,
290 this.primaryCandidates.get(account.getJid().asBareJid()));
291 }
292 }
293
294 public void retractSessionProposal(final Account account, final Jid with) {
295 synchronized (this.rtpSessionProposals) {
296 RtpSessionProposal matchingProposal = null;
297 for (RtpSessionProposal proposal : this.rtpSessionProposals.keySet()) {
298 if (proposal.account == account && with.asBareJid().equals(proposal.with)) {
299 matchingProposal = proposal;
300 break;
301 }
302 }
303 if (matchingProposal != null) {
304 this.rtpSessionProposals.remove(matchingProposal);
305 final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionRetract(matchingProposal);
306 writeLogMissedOutgoing(account, matchingProposal.with, matchingProposal.sessionId, null, System.currentTimeMillis());
307 mXmppConnectionService.sendMessagePacket(account, messagePacket);
308
309 }
310 }
311 }
312
313 public void proposeJingleRtpSession(final Account account, final Jid with) {
314 synchronized (this.rtpSessionProposals) {
315 for (Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry : this.rtpSessionProposals.entrySet()) {
316 RtpSessionProposal proposal = entry.getKey();
317 if (proposal.account == account && with.asBareJid().equals(proposal.with)) {
318 final DeviceDiscoveryState preexistingState = entry.getValue();
319 if (preexistingState != null && preexistingState != DeviceDiscoveryState.FAILED) {
320 mXmppConnectionService.notifyJingleRtpConnectionUpdate(
321 account,
322 with,
323 proposal.sessionId,
324 preexistingState.toEndUserState()
325 );
326 return;
327 }
328 }
329 }
330 final RtpSessionProposal proposal = RtpSessionProposal.of(account, with.asBareJid());
331 this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING);
332 mXmppConnectionService.notifyJingleRtpConnectionUpdate(
333 account,
334 proposal.with,
335 proposal.sessionId,
336 RtpEndUserState.FINDING_DEVICE
337 );
338 final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionProposal(proposal);
339 Log.d(Config.LOGTAG, messagePacket.toString());
340 mXmppConnectionService.sendMessagePacket(account, messagePacket);
341 }
342 }
343
344 public void deliverIbbPacket(Account account, IqPacket packet) {
345 final String sid;
346 final Element payload;
347 if (packet.hasChild("open", Namespace.IBB)) {
348 payload = packet.findChild("open", Namespace.IBB);
349 sid = payload.getAttribute("sid");
350 } else if (packet.hasChild("data", Namespace.IBB)) {
351 payload = packet.findChild("data", Namespace.IBB);
352 sid = payload.getAttribute("sid");
353 } else if (packet.hasChild("close", Namespace.IBB)) {
354 payload = packet.findChild("close", Namespace.IBB);
355 sid = payload.getAttribute("sid");
356 } else {
357 payload = null;
358 sid = null;
359 }
360 if (sid != null) {
361 for (final AbstractJingleConnection connection : this.connections.values()) {
362 if (connection instanceof JingleFileTransferConnection) {
363 final JingleFileTransferConnection fileTransfer = (JingleFileTransferConnection) connection;
364 final JingleTransport transport = fileTransfer.getTransport();
365 if (transport instanceof JingleInBandTransport) {
366 final JingleInBandTransport inBandTransport = (JingleInBandTransport) transport;
367 if (inBandTransport.matches(account, sid)) {
368 inBandTransport.deliverPayload(packet, payload);
369 }
370 return;
371 }
372 }
373 }
374 }
375 Log.d(Config.LOGTAG, "unable to deliver ibb packet: " + packet.toString());
376 account.getXmppConnection().sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null);
377 }
378
379 public void cancelInTransmission() {
380 for (AbstractJingleConnection connection : this.connections.values()) {
381 /*if (connection.getJingleStatus() == JingleFileTransferConnection.JINGLE_STATUS_TRANSMITTING) {
382 connection.abort("connectivity-error");
383 }*/
384 }
385 }
386
387 public WeakReference<JingleRtpConnection> findJingleRtpConnection(Account account, Jid with, String sessionId) {
388 final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, Jid.ofEscaped(with), sessionId);
389 final AbstractJingleConnection connection = connections.get(id);
390 if (connection instanceof JingleRtpConnection) {
391 return new WeakReference<>((JingleRtpConnection) connection);
392 }
393 return null;
394 }
395
396 public void updateProposedSessionDiscovered(Account account, Jid from, String sessionId, final DeviceDiscoveryState target) {
397 final RtpSessionProposal sessionProposal = new RtpSessionProposal(account, from.asBareJid(), sessionId);
398 synchronized (this.rtpSessionProposals) {
399 final DeviceDiscoveryState currentState = rtpSessionProposals.get(sessionProposal);
400 if (currentState == null) {
401 Log.d(Config.LOGTAG, "unable to find session proposal for session id " + sessionId);
402 return;
403 }
404 if (currentState == DeviceDiscoveryState.DISCOVERED) {
405 Log.d(Config.LOGTAG, "session proposal already at discovered. not going to fall back");
406 return;
407 }
408 this.rtpSessionProposals.put(sessionProposal, target);
409 mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, sessionProposal.with, sessionProposal.sessionId, target.toEndUserState());
410 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": flagging session " + sessionId + " as " + target);
411 }
412 }
413
414 public void rejectRtpSession(final String sessionId) {
415 for (final AbstractJingleConnection connection : this.connections.values()) {
416 if (connection.getId().sessionId.equals(sessionId)) {
417 if (connection instanceof JingleRtpConnection) {
418 ((JingleRtpConnection) connection).rejectCall();
419 }
420 }
421 }
422 }
423
424 public void endRtpSession(final String sessionId) {
425 for (final AbstractJingleConnection connection : this.connections.values()) {
426 if (connection.getId().sessionId.equals(sessionId)) {
427 if (connection instanceof JingleRtpConnection) {
428 ((JingleRtpConnection) connection).endCall();
429 }
430 }
431 }
432 }
433
434 public void failProceed(Account account, final Jid with, String sessionId) {
435 final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with, sessionId);
436 final AbstractJingleConnection existingJingleConnection = connections.get(id);
437 if (existingJingleConnection instanceof JingleRtpConnection) {
438 ((JingleRtpConnection) existingJingleConnection).deliverFailedProceed();
439 }
440 }
441
442 public enum DeviceDiscoveryState {
443 SEARCHING, DISCOVERED, FAILED;
444
445 public RtpEndUserState toEndUserState() {
446 switch (this) {
447 case SEARCHING:
448 return RtpEndUserState.FINDING_DEVICE;
449 case DISCOVERED:
450 return RtpEndUserState.RINGING;
451 default:
452 return RtpEndUserState.CONNECTIVITY_ERROR;
453 }
454 }
455 }
456
457 public static class RtpSessionProposal {
458 public final Jid with;
459 public final String sessionId;
460 private final Account account;
461
462 private RtpSessionProposal(Account account, Jid with, String sessionId) {
463 this.account = account;
464 this.with = with;
465 this.sessionId = sessionId;
466 }
467
468 public static RtpSessionProposal of(Account account, Jid with) {
469 return new RtpSessionProposal(account, with, nextRandomId());
470 }
471
472 @Override
473 public boolean equals(Object o) {
474 if (this == o) return true;
475 if (o == null || getClass() != o.getClass()) return false;
476 RtpSessionProposal proposal = (RtpSessionProposal) o;
477 return Objects.equal(account.getJid(), proposal.account.getJid()) &&
478 Objects.equal(with, proposal.with) &&
479 Objects.equal(sessionId, proposal.sessionId);
480 }
481
482 @Override
483 public int hashCode() {
484 return Objects.hashCode(account.getJid(), with, sessionId);
485 }
486 }
487}