1package eu.siacs.conversations.xmpp.jingle;
2
3import androidx.annotation.NonNull;
4import com.google.common.base.MoreObjects;
5import com.google.common.base.Objects;
6import com.google.common.base.Preconditions;
7import com.google.common.base.Predicates;
8import com.google.common.base.Strings;
9import com.google.common.collect.Collections2;
10import com.google.common.collect.ImmutableMap;
11import com.google.common.collect.ImmutableMultimap;
12import com.google.common.collect.ImmutableSet;
13import com.google.common.collect.Iterables;
14import com.google.common.collect.Maps;
15import com.google.common.collect.Sets;
16import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
17import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
18import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
19import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
20import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
21import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
22import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
23import im.conversations.android.xmpp.model.jingle.Jingle;
24import java.util.Collection;
25import java.util.HashMap;
26import java.util.LinkedHashMap;
27import java.util.List;
28import java.util.Map;
29import java.util.Set;
30
31public class RtpContentMap extends AbstractContentMap<RtpDescription, IceUdpTransportInfo> {
32
33 public RtpContentMap(
34 Group group,
35 Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> contents) {
36 super(group, contents);
37 }
38
39 public static RtpContentMap of(final Jingle jinglePacket) {
40 final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> contents =
41 of(jinglePacket.getJingleContents());
42 if (isOmemoVerified(contents)) {
43 return new OmemoVerifiedRtpContentMap(jinglePacket.getGroup(), contents);
44 } else {
45 return new RtpContentMap(jinglePacket.getGroup(), contents);
46 }
47 }
48
49 private static boolean isOmemoVerified(
50 Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> contents) {
51 final Collection<DescriptionTransport<RtpDescription, IceUdpTransportInfo>> values =
52 contents.values();
53 if (values.isEmpty()) {
54 return false;
55 }
56 for (final DescriptionTransport<RtpDescription, IceUdpTransportInfo> descriptionTransport :
57 values) {
58 if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) {
59 continue;
60 }
61 return false;
62 }
63 return true;
64 }
65
66 public static RtpContentMap of(
67 final SessionDescription sessionDescription, final boolean isInitiator) {
68 final ImmutableMap.Builder<
69 String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
70 contentMapBuilder = new ImmutableMap.Builder<>();
71 for (SessionDescription.Media media : sessionDescription.media) {
72 final String id = Iterables.getFirst(media.attributes.get("mid"), null);
73 Preconditions.checkNotNull(id, "media has no mid");
74 contentMapBuilder.put(id, of(sessionDescription, isInitiator, media));
75 }
76 final String groupAttribute =
77 Iterables.getFirst(sessionDescription.attributes.get("group"), null);
78 final Group group = groupAttribute == null ? null : Group.ofSdpString(groupAttribute);
79 return new RtpContentMap(group, contentMapBuilder.build());
80 }
81
82 public Set<Media> getMedia() {
83 return Sets.newHashSet(
84 Collections2.transform(
85 contents.values(),
86 input -> {
87 final RtpDescription rtpDescription =
88 input == null ? null : input.description;
89 return rtpDescription == null
90 ? Media.UNKNOWN
91 : input.description.getMedia();
92 }));
93 }
94
95 void requireDTLSFingerprint() {
96 requireDTLSFingerprint(false);
97 }
98
99 void requireDTLSFingerprint(final boolean requireActPass) {
100 if (this.contents.isEmpty()) {
101 throw new IllegalStateException("No contents available");
102 }
103 for (Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> entry :
104 this.contents.entrySet()) {
105 final IceUdpTransportInfo transport = entry.getValue().transport;
106 final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
107 if (fingerprint == null
108 || Strings.isNullOrEmpty(fingerprint.getContent())
109 || Strings.isNullOrEmpty(fingerprint.getHash())) {
110 throw new SecurityException(
111 String.format(
112 "Use of DTLS-SRTP (XEP-0320) is required for content %s",
113 entry.getKey()));
114 }
115 final IceUdpTransportInfo.Setup setup = fingerprint.getSetup();
116 if (setup == null) {
117 throw new SecurityException(
118 String.format(
119 "Use of DTLS-SRTP (XEP-0320) is required for content %s but missing"
120 + " setup attribute",
121 entry.getKey()));
122 }
123 if (requireActPass && setup != IceUdpTransportInfo.Setup.ACTPASS) {
124 throw new SecurityException(
125 "Initiator needs to offer ACTPASS as setup for DTLS-SRTP (XEP-0320)");
126 }
127 }
128 }
129
130 RtpContentMap transportInfo(
131 final String contentName, final IceUdpTransportInfo.Candidate candidate) {
132 final DescriptionTransport<RtpDescription, IceUdpTransportInfo> descriptionTransport =
133 contents.get(contentName);
134 final IceUdpTransportInfo transportInfo =
135 descriptionTransport == null ? null : descriptionTransport.transport;
136 if (transportInfo == null) {
137 throw new IllegalArgumentException(
138 "Unable to find transport info for content name " + contentName);
139 }
140 final IceUdpTransportInfo newTransportInfo = transportInfo.cloneWrapper();
141 newTransportInfo.addChild(candidate);
142 return new RtpContentMap(
143 null,
144 ImmutableMap.of(
145 contentName,
146 new DescriptionTransport<>(
147 descriptionTransport.senders, null, newTransportInfo)));
148 }
149
150 RtpContentMap transportInfo() {
151 return new RtpContentMap(
152 null,
153 Maps.transformValues(
154 contents,
155 dt ->
156 new DescriptionTransport<>(
157 dt.senders, null, dt.transport.cloneWrapper())));
158 }
159
160 RtpContentMap withCandidates(
161 ImmutableMultimap<String, IceUdpTransportInfo.Candidate> candidates) {
162 final ImmutableMap.Builder<
163 String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
164 contentBuilder = new ImmutableMap.Builder<>();
165 for (final Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
166 entry : this.contents.entrySet()) {
167 final String name = entry.getKey();
168 final DescriptionTransport<RtpDescription, IceUdpTransportInfo> descriptionTransport =
169 entry.getValue();
170 final var transport = descriptionTransport.transport;
171 contentBuilder.put(
172 name,
173 new DescriptionTransport<>(
174 descriptionTransport.senders,
175 descriptionTransport.description,
176 transport.withCandidates(candidates.get(name))));
177 }
178 return new RtpContentMap(group, contentBuilder.build());
179 }
180
181 public IceUdpTransportInfo.Credentials getDistinctCredentials() {
182 final Set<IceUdpTransportInfo.Credentials> allCredentials = getCredentials();
183 final IceUdpTransportInfo.Credentials credentials =
184 Iterables.getFirst(allCredentials, null);
185 if (allCredentials.size() == 1 && credentials != null) {
186 if (Strings.isNullOrEmpty(credentials.password)
187 || Strings.isNullOrEmpty(credentials.ufrag)) {
188 throw new IllegalStateException("Credentials are missing password or ufrag");
189 }
190 return credentials;
191 }
192 throw new IllegalStateException("Content map does not have distinct credentials");
193 }
194
195 private Set<String> getCombinedIceOptions() {
196 final Collection<List<String>> combinedIceOptions =
197 Collections2.transform(contents.values(), dt -> dt.transport.getIceOptions());
198 return ImmutableSet.copyOf(Iterables.concat(combinedIceOptions));
199 }
200
201 public Set<IceUdpTransportInfo.Credentials> getCredentials() {
202 final Set<IceUdpTransportInfo.Credentials> credentials =
203 ImmutableSet.copyOf(
204 Collections2.transform(
205 contents.values(), dt -> dt.transport.getCredentials()));
206 if (credentials.isEmpty()) {
207 throw new IllegalStateException("Content map does not have any credentials");
208 }
209 return credentials;
210 }
211
212 public IceUdpTransportInfo.Credentials getCredentials(final String contentName) {
213 final var descriptionTransport = this.contents.get(contentName);
214 if (descriptionTransport == null) {
215 throw new IllegalArgumentException(
216 String.format(
217 "Unable to find transport info for content name %s", contentName));
218 }
219 return descriptionTransport.transport.getCredentials();
220 }
221
222 public IceUdpTransportInfo.Setup getDtlsSetup() {
223 final Set<IceUdpTransportInfo.Setup> setups =
224 ImmutableSet.copyOf(
225 Collections2.transform(
226 contents.values(), dt -> dt.transport.getFingerprint().getSetup()));
227 final IceUdpTransportInfo.Setup setup = Iterables.getFirst(setups, null);
228 if (setups.size() == 1 && setup != null) {
229 return setup;
230 }
231 throw new IllegalStateException("Content map doesn't have distinct DTLS setup");
232 }
233
234 private DTLS getDistinctDtls() {
235 final Set<DTLS> dtlsSet =
236 ImmutableSet.copyOf(
237 Collections2.transform(
238 contents.values(),
239 dt -> {
240 final IceUdpTransportInfo.Fingerprint fp =
241 dt.transport.getFingerprint();
242 return new DTLS(fp.getHash(), fp.getSetup(), fp.getContent());
243 }));
244 final DTLS dtls = Iterables.getFirst(dtlsSet, null);
245 if (dtlsSet.size() == 1 && dtls != null) {
246 return dtls;
247 }
248 throw new IllegalStateException("Content map doesn't have distinct DTLS setup");
249 }
250
251 public boolean emptyCandidates() {
252 int count = 0;
253 for (final var descriptionTransport : contents.values()) {
254 count += descriptionTransport.transport.getCandidates().size();
255 }
256 return count == 0;
257 }
258
259 public boolean hasFullTransportInfo() {
260 return Collections2.transform(this.contents.values(), dt -> dt.transport.isStub())
261 .contains(false);
262 }
263
264 public RtpContentMap modifiedCredentials(
265 IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) {
266 final ImmutableMap.Builder<
267 String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
268 contentMapBuilder = new ImmutableMap.Builder<>();
269 for (final Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
270 content : contents.entrySet()) {
271 final var descriptionTransport = content.getValue();
272 final RtpDescription rtpDescription = descriptionTransport.description;
273 final IceUdpTransportInfo transportInfo = descriptionTransport.transport;
274 final IceUdpTransportInfo modifiedTransportInfo =
275 transportInfo.modifyCredentials(credentials, setup);
276 contentMapBuilder.put(
277 content.getKey(),
278 new DescriptionTransport<>(
279 descriptionTransport.senders, rtpDescription, modifiedTransportInfo));
280 }
281 return new RtpContentMap(this.group, contentMapBuilder.build());
282 }
283
284 public RtpContentMap modifiedSenders(final Content.Senders senders) {
285 return new RtpContentMap(
286 this.group,
287 Maps.transformValues(
288 contents,
289 dt -> new DescriptionTransport<>(senders, dt.description, dt.transport)));
290 }
291
292 public RtpContentMap modifiedSendersChecked(
293 final boolean isInitiator, final Map<String, Content.Senders> modification) {
294 final ImmutableMap.Builder<
295 String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
296 contentMapBuilder = new ImmutableMap.Builder<>();
297 for (final Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
298 content : contents.entrySet()) {
299 final String id = content.getKey();
300 final var descriptionTransport = content.getValue();
301 final Content.Senders currentSenders = descriptionTransport.senders;
302 final Content.Senders targetSenders = modification.get(id);
303 if (targetSenders == null || currentSenders == targetSenders) {
304 contentMapBuilder.put(id, descriptionTransport);
305 } else {
306 checkSenderModification(isInitiator, currentSenders, targetSenders);
307 contentMapBuilder.put(
308 id,
309 new DescriptionTransport<>(
310 targetSenders,
311 descriptionTransport.description,
312 descriptionTransport.transport));
313 }
314 }
315 return new RtpContentMap(this.group, contentMapBuilder.build());
316 }
317
318 private static void checkSenderModification(
319 final boolean isInitiator,
320 final Content.Senders current,
321 final Content.Senders target) {
322 if (isInitiator) {
323 // we were both sending and now other party only wants to receive
324 if (current == Content.Senders.BOTH && target == Content.Senders.INITIATOR) {
325 return;
326 }
327 // only we were sending but now other party wants to send too
328 if (current == Content.Senders.INITIATOR && target == Content.Senders.BOTH) {
329 return;
330 }
331 } else {
332 // we were both sending and now other party only wants to receive
333 if (current == Content.Senders.BOTH && target == Content.Senders.RESPONDER) {
334 return;
335 }
336 // only we were sending but now other party wants to send too
337 if (current == Content.Senders.RESPONDER && target == Content.Senders.BOTH) {
338 return;
339 }
340 }
341 throw new IllegalArgumentException(
342 String.format("Unsupported senders modification %s -> %s", current, target));
343 }
344
345 public RtpContentMap toContentModification(final Collection<String> modifications) {
346 return new RtpContentMap(
347 this.group, Maps.filterKeys(contents, Predicates.in(modifications)));
348 }
349
350 public RtpContentMap toStub() {
351 return new RtpContentMap(
352 null,
353 Maps.transformValues(
354 this.contents,
355 dt ->
356 new DescriptionTransport<>(
357 dt.senders,
358 RtpDescription.stub(dt.description.getMedia()),
359 IceUdpTransportInfo.STUB)));
360 }
361
362 public RtpContentMap activeContents() {
363 return new RtpContentMap(
364 group, Maps.filterValues(this.contents, dt -> dt.senders != Content.Senders.NONE));
365 }
366
367 public Diff diff(final RtpContentMap rtpContentMap) {
368 final Set<String> existingContentIds = this.contents.keySet();
369 final Set<String> newContentIds = rtpContentMap.contents.keySet();
370 return new Diff(
371 ImmutableSet.copyOf(Sets.difference(newContentIds, existingContentIds)),
372 ImmutableSet.copyOf(Sets.difference(existingContentIds, newContentIds)));
373 }
374
375 public boolean iceRestart(final RtpContentMap rtpContentMap) {
376 try {
377 return !getDistinctCredentials().equals(rtpContentMap.getDistinctCredentials());
378 } catch (final IllegalStateException e) {
379 return false;
380 }
381 }
382
383 public RtpContentMap addContent(
384 final RtpContentMap modification, final IceUdpTransportInfo.Setup setupOverwrite) {
385 final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> combined =
386 merge(contents, modification.contents);
387 final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
388 combinedFixedTransport =
389 Maps.transformValues(
390 combined,
391 dt -> {
392 final IceUdpTransportInfo iceUdpTransportInfo;
393 if (dt.transport.isStub()) {
394 final IceUdpTransportInfo.Credentials credentials =
395 getDistinctCredentials();
396 final Collection<String> iceOptions =
397 getCombinedIceOptions();
398 final DTLS dtls = getDistinctDtls();
399 iceUdpTransportInfo =
400 IceUdpTransportInfo.of(
401 credentials,
402 iceOptions,
403 setupOverwrite,
404 dtls.hash,
405 dtls.fingerprint);
406 } else {
407 final IceUdpTransportInfo.Fingerprint fp =
408 dt.transport.getFingerprint();
409 final IceUdpTransportInfo.Setup setup = fp.getSetup();
410 iceUdpTransportInfo =
411 IceUdpTransportInfo.of(
412 dt.transport.getCredentials(),
413 dt.transport.getIceOptions(),
414 setup == IceUdpTransportInfo.Setup.ACTPASS
415 ? setupOverwrite
416 : setup,
417 fp.getHash(),
418 fp.getContent());
419 }
420 return new DescriptionTransport<>(
421 dt.senders, dt.description, iceUdpTransportInfo);
422 });
423 return new RtpContentMap(modification.group, ImmutableMap.copyOf(combinedFixedTransport));
424 }
425
426 private static Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> merge(
427 final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> a,
428 final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> b) {
429 final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> combined =
430 new LinkedHashMap<>();
431 combined.putAll(a);
432 combined.putAll(b);
433 return ImmutableMap.copyOf(combined);
434 }
435
436 public static DescriptionTransport<RtpDescription, IceUdpTransportInfo> of(
437 final Content content) {
438 final GenericDescription description = content.getDescription();
439 final GenericTransportInfo transportInfo = content.getTransport();
440 final Content.Senders senders = content.getSenders();
441 final RtpDescription rtpDescription;
442 final IceUdpTransportInfo iceUdpTransportInfo;
443 if (description == null) {
444 rtpDescription = null;
445 } else if (description instanceof RtpDescription) {
446 rtpDescription = (RtpDescription) description;
447 } else {
448 throw new UnsupportedApplicationException("Content does not contain rtp description");
449 }
450 if (transportInfo instanceof IceUdpTransportInfo) {
451 iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo;
452 } else {
453 throw new UnsupportedTransportException("Content does not contain ICE-UDP transport");
454 }
455 return new DescriptionTransport<>(
456 senders,
457 rtpDescription,
458 OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo));
459 }
460
461 private static DescriptionTransport<RtpDescription, IceUdpTransportInfo> of(
462 final SessionDescription sessionDescription,
463 final boolean isInitiator,
464 final SessionDescription.Media media) {
465 final Content.Senders senders = Content.Senders.of(media, isInitiator);
466 final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media);
467 final IceUdpTransportInfo transportInfo = IceUdpTransportInfo.of(sessionDescription, media);
468 return new DescriptionTransport<>(senders, rtpDescription, transportInfo);
469 }
470
471 private static Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> of(
472 final Map<String, Content> contents) {
473 return ImmutableMap.copyOf(
474 Maps.transformValues(contents, content -> content == null ? null : of(content)));
475 }
476
477 public static final class Diff {
478 public final Set<String> added;
479 public final Set<String> removed;
480
481 private Diff(final Set<String> added, final Set<String> removed) {
482 this.added = added;
483 this.removed = removed;
484 }
485
486 public boolean hasModifications() {
487 return !this.added.isEmpty() || !this.removed.isEmpty();
488 }
489
490 public boolean isEmpty() {
491 return this.added.isEmpty() && this.removed.isEmpty();
492 }
493
494 @Override
495 @NonNull
496 public String toString() {
497 return MoreObjects.toStringHelper(this)
498 .add("added", added)
499 .add("removed", removed)
500 .toString();
501 }
502 }
503
504 public static final class DTLS {
505 public final String hash;
506 public final IceUdpTransportInfo.Setup setup;
507 public final String fingerprint;
508
509 private DTLS(String hash, IceUdpTransportInfo.Setup setup, String fingerprint) {
510 this.hash = hash;
511 this.setup = setup;
512 this.fingerprint = fingerprint;
513 }
514
515 @Override
516 public boolean equals(Object o) {
517 if (this == o) return true;
518 if (o == null || getClass() != o.getClass()) return false;
519 DTLS dtls = (DTLS) o;
520 return Objects.equal(hash, dtls.hash)
521 && setup == dtls.setup
522 && Objects.equal(fingerprint, dtls.fingerprint);
523 }
524
525 @Override
526 public int hashCode() {
527 return Objects.hashCode(hash, setup, fingerprint);
528 }
529 }
530}