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