RtpContentMap.java

  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}