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.ImmutableSet;
 12import com.google.common.collect.Iterables;
 13import com.google.common.collect.Maps;
 14import com.google.common.collect.Sets;
 15
 16import java.util.Collection;
 17import java.util.List;
 18import java.util.Map;
 19import java.util.Set;
 20
 21import javax.annotation.Nonnull;
 22
 23import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
 24import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
 25import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
 26import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
 27import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
 28import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
 29import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
 30import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
 31
 32public class RtpContentMap {
 33
 34    public final Group group;
 35    public final Map<String, DescriptionTransport> contents;
 36
 37    public RtpContentMap(Group group, Map<String, DescriptionTransport> contents) {
 38        this.group = group;
 39        this.contents = contents;
 40    }
 41
 42    public static RtpContentMap of(final JinglePacket jinglePacket) {
 43        final Map<String, DescriptionTransport> contents =
 44                DescriptionTransport.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(Map<String, DescriptionTransport> contents) {
 53        final Collection<DescriptionTransport> values = contents.values();
 54        if (values.size() == 0) {
 55            return false;
 56        }
 57        for (final DescriptionTransport descriptionTransport : values) {
 58            if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) {
 59                continue;
 60            }
 61            return false;
 62        }
 63        return true;
 64    }
 65
 66    public static RtpContentMap of(
 67            final SessionDescription sessionDescription, final boolean isInitiator) {
 68        final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
 69                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(
 74                    id, DescriptionTransport.of(sessionDescription, isInitiator, media));
 75        }
 76        final String groupAttribute =
 77                Iterables.getFirst(sessionDescription.attributes.get("group"), null);
 78        final Group group = groupAttribute == null ? null : Group.ofSdpString(groupAttribute);
 79        return new RtpContentMap(group, contentMapBuilder.build());
 80    }
 81
 82    public Set<Media> getMedia() {
 83        return Sets.newHashSet(
 84                Collections2.transform(
 85                        contents.values(),
 86                        input -> {
 87                            final RtpDescription rtpDescription =
 88                                    input == null ? null : input.description;
 89                            return rtpDescription == null
 90                                    ? Media.UNKNOWN
 91                                    : input.description.getMedia();
 92                        }));
 93    }
 94
 95    public List<String> getNames() {
 96        return ImmutableList.copyOf(contents.keySet());
 97    }
 98
 99    void requireContentDescriptions() {
100        if (this.contents.size() == 0) {
101            throw new IllegalStateException("No contents available");
102        }
103        for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
104            if (entry.getValue().description == null) {
105                throw new IllegalStateException(
106                        String.format("%s is lacking content description", entry.getKey()));
107            }
108        }
109    }
110
111    void requireDTLSFingerprint() {
112        requireDTLSFingerprint(false);
113    }
114
115    void requireDTLSFingerprint(final boolean requireActPass) {
116        if (this.contents.size() == 0) {
117            throw new IllegalStateException("No contents available");
118        }
119        for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
120            final IceUdpTransportInfo transport = entry.getValue().transport;
121            final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
122            if (fingerprint == null
123                    || Strings.isNullOrEmpty(fingerprint.getContent())
124                    || Strings.isNullOrEmpty(fingerprint.getHash())) {
125                throw new SecurityException(
126                        String.format(
127                                "Use of DTLS-SRTP (XEP-0320) is required for content %s",
128                                entry.getKey()));
129            }
130            final IceUdpTransportInfo.Setup setup = fingerprint.getSetup();
131            if (setup == null) {
132                throw new SecurityException(
133                        String.format(
134                                "Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute",
135                                entry.getKey()));
136            }
137            if (requireActPass && setup != IceUdpTransportInfo.Setup.ACTPASS) {
138                throw new SecurityException(
139                        "Initiator needs to offer ACTPASS as setup for DTLS-SRTP (XEP-0320)");
140            }
141        }
142    }
143
144    JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) {
145        final JinglePacket jinglePacket = new JinglePacket(action, sessionId);
146        if (this.group != null) {
147            jinglePacket.addGroup(this.group);
148        }
149        for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
150            final DescriptionTransport descriptionTransport = entry.getValue();
151            final Content content =
152                    new Content(
153                            Content.Creator.INITIATOR,
154                            descriptionTransport.senders,
155                            entry.getKey());
156            if (descriptionTransport.description != null) {
157                content.addChild(descriptionTransport.description);
158            }
159            content.addChild(descriptionTransport.transport);
160            jinglePacket.addJingleContent(content);
161        }
162        return jinglePacket;
163    }
164
165    RtpContentMap transportInfo(
166            final String contentName, final IceUdpTransportInfo.Candidate candidate) {
167        final RtpContentMap.DescriptionTransport descriptionTransport = contents.get(contentName);
168        final IceUdpTransportInfo transportInfo =
169                descriptionTransport == null ? null : descriptionTransport.transport;
170        if (transportInfo == null) {
171            throw new IllegalArgumentException(
172                    "Unable to find transport info for content name " + contentName);
173        }
174        final IceUdpTransportInfo newTransportInfo = transportInfo.cloneWrapper();
175        newTransportInfo.addChild(candidate);
176        return new RtpContentMap(
177                null,
178                ImmutableMap.of(
179                        contentName,
180                        new DescriptionTransport(
181                                descriptionTransport.senders, null, newTransportInfo)));
182    }
183
184    RtpContentMap transportInfo() {
185        return new RtpContentMap(
186                null,
187                Maps.transformValues(
188                        contents,
189                        dt ->
190                                new DescriptionTransport(
191                                        dt.senders, null, dt.transport.cloneWrapper())));
192    }
193
194    public IceUdpTransportInfo.Credentials getDistinctCredentials() {
195        final Set<IceUdpTransportInfo.Credentials> allCredentials = getCredentials();
196        final IceUdpTransportInfo.Credentials credentials =
197                Iterables.getFirst(allCredentials, null);
198        if (allCredentials.size() == 1 && credentials != null) {
199            if (Strings.isNullOrEmpty(credentials.password)
200                    || Strings.isNullOrEmpty(credentials.ufrag)) {
201                throw new IllegalStateException("Credentials are missing password or ufrag");
202            }
203            return credentials;
204        }
205        throw new IllegalStateException("Content map does not have distinct credentials");
206    }
207
208    public Set<IceUdpTransportInfo.Credentials> getCredentials() {
209        final Set<IceUdpTransportInfo.Credentials> credentials =
210                ImmutableSet.copyOf(
211                        Collections2.transform(
212                                contents.values(), dt -> dt.transport.getCredentials()));
213        if (credentials.isEmpty()) {
214            throw new IllegalStateException("Content map does not have any credentials");
215        }
216        return credentials;
217    }
218
219    public IceUdpTransportInfo.Credentials getCredentials(final String contentName) {
220        final DescriptionTransport descriptionTransport = this.contents.get(contentName);
221        if (descriptionTransport == null) {
222            throw new IllegalArgumentException(
223                    String.format(
224                            "Unable to find transport info for content name %s", contentName));
225        }
226        return descriptionTransport.transport.getCredentials();
227    }
228
229    public IceUdpTransportInfo.Setup getDtlsSetup() {
230        final Set<IceUdpTransportInfo.Setup> setups =
231                ImmutableSet.copyOf(
232                        Collections2.transform(
233                                contents.values(), dt -> dt.transport.getFingerprint().getSetup()));
234        final IceUdpTransportInfo.Setup setup = Iterables.getFirst(setups, null);
235        if (setups.size() == 1 && setup != null) {
236            return setup;
237        }
238        throw new IllegalStateException("Content map doesn't have distinct DTLS setup");
239    }
240
241    private DTLS getDistinctDtls() {
242        final Set<DTLS> dtlsSet =
243                ImmutableSet.copyOf(
244                        Collections2.transform(
245                                contents.values(),
246                                dt -> {
247                                    final IceUdpTransportInfo.Fingerprint fp =
248                                            dt.transport.getFingerprint();
249                                    return new DTLS(fp.getHash(), fp.getSetup(), fp.getContent());
250                                }));
251        final DTLS dtls = Iterables.getFirst(dtlsSet, null);
252        if (dtlsSet.size() == 1 && dtls != null) {
253            return dtls;
254        }
255        throw new IllegalStateException("Content map doesn't have distinct DTLS setup");
256    }
257
258    public boolean emptyCandidates() {
259        int count = 0;
260        for (DescriptionTransport descriptionTransport : contents.values()) {
261            count += descriptionTransport.transport.getCandidates().size();
262        }
263        return count == 0;
264    }
265
266    public RtpContentMap modifiedCredentials(
267            IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) {
268        final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
269                new ImmutableMap.Builder<>();
270        for (final Map.Entry<String, DescriptionTransport> content : contents.entrySet()) {
271            final DescriptionTransport descriptionTransport = content.getValue();
272            final RtpDescription rtpDescription = descriptionTransport.description;
273            final IceUdpTransportInfo transportInfo = descriptionTransport.transport;
274            final IceUdpTransportInfo modifiedTransportInfo =
275                    transportInfo.modifyCredentials(credentials, setup);
276            contentMapBuilder.put(
277                    content.getKey(),
278                    new DescriptionTransport(
279                            descriptionTransport.senders, rtpDescription, modifiedTransportInfo));
280        }
281        return new RtpContentMap(this.group, contentMapBuilder.build());
282    }
283
284    public RtpContentMap toContentModification(final Collection<String> modifications) {
285        return new RtpContentMap(
286                this.group,
287                Maps.transformValues(
288                        Maps.filterKeys(contents, Predicates.in(modifications)),
289                        dt ->
290                                new DescriptionTransport(
291                                        dt.senders, dt.description, IceUdpTransportInfo.STUB)));
292    }
293
294    public Diff diff(final RtpContentMap rtpContentMap) {
295        final Set<String> existingContentIds = this.contents.keySet();
296        final Set<String> newContentIds = rtpContentMap.contents.keySet();
297        return new Diff(
298                ImmutableSet.copyOf(Sets.difference(newContentIds, existingContentIds)),
299                ImmutableSet.copyOf(Sets.difference(existingContentIds, newContentIds)));
300    }
301
302    public boolean iceRestart(final RtpContentMap rtpContentMap) {
303        try {
304            return !getDistinctCredentials().equals(rtpContentMap.getDistinctCredentials());
305        } catch (final IllegalStateException e) {
306            return false;
307        }
308    }
309
310    public RtpContentMap addContent(final RtpContentMap modification) {
311        final IceUdpTransportInfo.Credentials credentials = getDistinctCredentials();
312        final DTLS dtls = getDistinctDtls();
313        final IceUdpTransportInfo iceUdpTransportInfo =
314                IceUdpTransportInfo.of(credentials, dtls.setup, dtls.hash, dtls.fingerprint);
315        final Map<String, DescriptionTransport> combined =
316                new ImmutableMap.Builder<String, DescriptionTransport>()
317                        .putAll(contents)
318                        .putAll(
319                                Maps.transformValues(
320                                        modification.contents,
321                                        dt ->
322                                                new DescriptionTransport(
323                                                        dt.senders,
324                                                        dt.description,
325                                                        iceUdpTransportInfo)))
326                        .build();
327        return new RtpContentMap(modification.group, combined);
328    }
329
330    public static class DescriptionTransport {
331        public final Content.Senders senders;
332        public final RtpDescription description;
333        public final IceUdpTransportInfo transport;
334
335        public DescriptionTransport(
336                final Content.Senders senders,
337                final RtpDescription description,
338                final IceUdpTransportInfo transport) {
339            this.senders = senders;
340            this.description = description;
341            this.transport = transport;
342        }
343
344        public static DescriptionTransport of(final Content content) {
345            final GenericDescription description = content.getDescription();
346            final GenericTransportInfo transportInfo = content.getTransport();
347            final Content.Senders senders = content.getSenders();
348            final RtpDescription rtpDescription;
349            final IceUdpTransportInfo iceUdpTransportInfo;
350            if (description == null) {
351                rtpDescription = null;
352            } else if (description instanceof RtpDescription) {
353                rtpDescription = (RtpDescription) description;
354            } else {
355                throw new UnsupportedApplicationException(
356                        "Content does not contain rtp description");
357            }
358            if (transportInfo instanceof IceUdpTransportInfo) {
359                iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo;
360            } else {
361                throw new UnsupportedTransportException(
362                        "Content does not contain ICE-UDP transport");
363            }
364            return new DescriptionTransport(
365                    senders,
366                    rtpDescription,
367                    OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo));
368        }
369
370        private static DescriptionTransport of(
371                final SessionDescription sessionDescription,
372                final boolean isInitiator,
373                final SessionDescription.Media media) {
374            final Content.Senders senders = Content.Senders.of(media, isInitiator);
375            final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media);
376            final IceUdpTransportInfo transportInfo =
377                    IceUdpTransportInfo.of(sessionDescription, media);
378            return new DescriptionTransport(senders, rtpDescription, transportInfo);
379        }
380
381        public static Map<String, DescriptionTransport> of(final Map<String, Content> contents) {
382            return ImmutableMap.copyOf(
383                    Maps.transformValues(
384                            contents, content -> content == null ? null : of(content)));
385        }
386    }
387
388    public static class UnsupportedApplicationException extends IllegalArgumentException {
389        UnsupportedApplicationException(String message) {
390            super(message);
391        }
392    }
393
394    public static class UnsupportedTransportException extends IllegalArgumentException {
395        UnsupportedTransportException(String message) {
396            super(message);
397        }
398    }
399
400    public static final class Diff {
401        public final Set<String> added;
402        public final Set<String> removed;
403
404        private Diff(final Set<String> added, final Set<String> removed) {
405            this.added = added;
406            this.removed = removed;
407        }
408
409        public boolean hasModifications() {
410            return !this.added.isEmpty() || !this.removed.isEmpty();
411        }
412
413        @Override
414        @Nonnull
415        public String toString() {
416            return MoreObjects.toStringHelper(this)
417                    .add("added", added)
418                    .add("removed", removed)
419                    .toString();
420        }
421    }
422
423    public static final class DTLS {
424        public final String hash;
425        public final IceUdpTransportInfo.Setup setup;
426        public final String fingerprint;
427
428        private DTLS(String hash, IceUdpTransportInfo.Setup setup, String fingerprint) {
429            this.hash = hash;
430            this.setup = setup;
431            this.fingerprint = fingerprint;
432        }
433
434        @Override
435        public boolean equals(Object o) {
436            if (this == o) return true;
437            if (o == null || getClass() != o.getClass()) return false;
438            DTLS dtls = (DTLS) o;
439            return Objects.equal(hash, dtls.hash)
440                    && setup == dtls.setup
441                    && Objects.equal(fingerprint, dtls.fingerprint);
442        }
443
444        @Override
445        public int hashCode() {
446            return Objects.hashCode(hash, setup, fingerprint);
447        }
448    }
449}