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