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