RtpContentMap.java

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