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