RtpContentMap.java

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