RtpContentMap.java

  1package eu.siacs.conversations.xmpp.jingle;
  2
  3import com.google.common.base.Function;
  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 org.checkerframework.checker.nullness.compatqual.NullableDecl;
 15
 16import java.util.Collection;
 17import java.util.List;
 18import java.util.Map;
 19import java.util.Objects;
 20import java.util.Set;
 21
 22import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
 23import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
 24import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
 25import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
 26import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
 27import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
 28import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
 29import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
 30
 31public class RtpContentMap {
 32
 33    public final Group group;
 34    public final Map<String, DescriptionTransport> contents;
 35
 36    public RtpContentMap(Group group, Map<String, DescriptionTransport> contents) {
 37        this.group = group;
 38        this.contents = contents;
 39    }
 40
 41    public static RtpContentMap of(final JinglePacket jinglePacket) {
 42        final Map<String, DescriptionTransport> contents = 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(final SessionDescription sessionDescription) {
 65        final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder = new ImmutableMap.Builder<>();
 66        for (SessionDescription.Media media : sessionDescription.media) {
 67            final String id = Iterables.getFirst(media.attributes.get("mid"), null);
 68            Preconditions.checkNotNull(id, "media has no mid");
 69            contentMapBuilder.put(id, DescriptionTransport.of(sessionDescription, media));
 70        }
 71        final String groupAttribute = Iterables.getFirst(sessionDescription.attributes.get("group"), null);
 72        final Group group = groupAttribute == null ? null : Group.ofSdpString(groupAttribute);
 73        return new RtpContentMap(group, contentMapBuilder.build());
 74    }
 75
 76    public Set<Media> getMedia() {
 77        return Sets.newHashSet(Collections2.transform(contents.values(), input -> {
 78            final RtpDescription rtpDescription = input == null ? null : input.description;
 79            return rtpDescription == null ? Media.UNKNOWN : input.description.getMedia();
 80        }));
 81    }
 82
 83    public List<String> getNames() {
 84        return ImmutableList.copyOf(contents.keySet());
 85    }
 86
 87    void requireContentDescriptions() {
 88        if (this.contents.size() == 0) {
 89            throw new IllegalStateException("No contents available");
 90        }
 91        for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
 92            if (entry.getValue().description == null) {
 93                throw new IllegalStateException(String.format("%s is lacking content description", entry.getKey()));
 94            }
 95        }
 96    }
 97
 98    void requireDTLSFingerprint() {
 99        requireDTLSFingerprint(false);
100    }
101
102    void requireDTLSFingerprint(final boolean requireActPass) {
103        if (this.contents.size() == 0) {
104            throw new IllegalStateException("No contents available");
105        }
106        for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
107            final IceUdpTransportInfo transport = entry.getValue().transport;
108            final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
109            if (fingerprint == null || Strings.isNullOrEmpty(fingerprint.getContent()) || Strings.isNullOrEmpty(fingerprint.getHash())) {
110                throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s", entry.getKey()));
111            }
112            final IceUdpTransportInfo.Setup setup = fingerprint.getSetup();
113            if (setup == null) {
114                throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute", entry.getKey()));
115            }
116            if (requireActPass && setup != IceUdpTransportInfo.Setup.ACTPASS) {
117                 throw new SecurityException("Initiator needs to offer ACTPASS as setup for DTLS-SRTP (XEP-0320)");
118            }
119        }
120    }
121
122    JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) {
123        final JinglePacket jinglePacket = new JinglePacket(action, sessionId);
124        if (this.group != null) {
125            jinglePacket.addGroup(this.group);
126        }
127        for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
128            final Content content = new Content(Content.Creator.INITIATOR, entry.getKey());
129            if (entry.getValue().description != null) {
130                content.addChild(entry.getValue().description);
131            }
132            content.addChild(entry.getValue().transport);
133            jinglePacket.addJingleContent(content);
134        }
135        return jinglePacket;
136    }
137
138    RtpContentMap transportInfo(final String contentName, final IceUdpTransportInfo.Candidate candidate) {
139        final RtpContentMap.DescriptionTransport descriptionTransport = contents.get(contentName);
140        final IceUdpTransportInfo transportInfo = descriptionTransport == null ? null : descriptionTransport.transport;
141        if (transportInfo == null) {
142            throw new IllegalArgumentException("Unable to find transport info for content name " + contentName);
143        }
144        final IceUdpTransportInfo newTransportInfo = transportInfo.cloneWrapper();
145        newTransportInfo.addChild(candidate);
146        return new RtpContentMap(null, ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo)));
147    }
148
149    RtpContentMap transportInfo() {
150        return new RtpContentMap(
151                null,
152                Maps.transformValues(contents, dt -> new DescriptionTransport(null, dt.transport.cloneWrapper()))
153        );
154    }
155
156    public IceUdpTransportInfo.Credentials getCredentials() {
157        final Set<IceUdpTransportInfo.Credentials> allCredentials = ImmutableSet.copyOf(Collections2.transform(
158                contents.values(),
159                dt -> dt.transport.getCredentials()
160        ));
161        final IceUdpTransportInfo.Credentials credentials = Iterables.getFirst(allCredentials, null);
162        if (allCredentials.size() == 1 && credentials != null) {
163            return credentials;
164        }
165        throw new IllegalStateException("Content map does not have distinct credentials");
166    }
167
168    public IceUdpTransportInfo.Setup getDtlsSetup() {
169        final Set<IceUdpTransportInfo.Setup> setups = ImmutableSet.copyOf(Collections2.transform(
170                contents.values(),
171                dt -> dt.transport.getFingerprint().getSetup()
172        ));
173        final IceUdpTransportInfo.Setup setup = Iterables.getFirst(setups, null);
174        if (setups.size() == 1 && setup != null) {
175            return setup;
176        }
177        throw new IllegalStateException("Content map doesn't have distinct DTLS setup");
178    }
179
180    public boolean emptyCandidates() {
181        int count = 0;
182        for (DescriptionTransport descriptionTransport : contents.values()) {
183            count += descriptionTransport.transport.getCandidates().size();
184        }
185        return count == 0;
186    }
187
188    public RtpContentMap modifiedCredentials(IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) {
189        final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder = new ImmutableMap.Builder<>();
190        for (final Map.Entry<String, DescriptionTransport> content : contents.entrySet()) {
191            final RtpDescription rtpDescription = content.getValue().description;
192            IceUdpTransportInfo transportInfo = content.getValue().transport;
193            final IceUdpTransportInfo modifiedTransportInfo = transportInfo.modifyCredentials(credentials, setup);
194            contentMapBuilder.put(content.getKey(), new DescriptionTransport(rtpDescription, modifiedTransportInfo));
195        }
196        return new RtpContentMap(this.group, contentMapBuilder.build());
197    }
198
199    public static class DescriptionTransport {
200        public final RtpDescription description;
201        public final IceUdpTransportInfo transport;
202
203        public DescriptionTransport(final RtpDescription description, final IceUdpTransportInfo transport) {
204            this.description = description;
205            this.transport = transport;
206        }
207
208        public static DescriptionTransport of(final Content content) {
209            final GenericDescription description = content.getDescription();
210            final GenericTransportInfo transportInfo = content.getTransport();
211            final RtpDescription rtpDescription;
212            final IceUdpTransportInfo iceUdpTransportInfo;
213            if (description == null) {
214                rtpDescription = null;
215            } else if (description instanceof RtpDescription) {
216                rtpDescription = (RtpDescription) description;
217            } else {
218                throw new UnsupportedApplicationException("Content does not contain rtp description");
219            }
220            if (transportInfo instanceof IceUdpTransportInfo) {
221                iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo;
222            } else {
223                throw new UnsupportedTransportException("Content does not contain ICE-UDP transport");
224            }
225            return new DescriptionTransport(
226                    rtpDescription,
227                    OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo)
228            );
229        }
230
231        public static DescriptionTransport of(final SessionDescription sessionDescription, final SessionDescription.Media media) {
232            final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media);
233            final IceUdpTransportInfo transportInfo = IceUdpTransportInfo.of(sessionDescription, media);
234            return new DescriptionTransport(rtpDescription, transportInfo);
235        }
236
237        public static Map<String, DescriptionTransport> of(final Map<String, Content> contents) {
238            return ImmutableMap.copyOf(Maps.transformValues(contents, new Function<Content, DescriptionTransport>() {
239                @NullableDecl
240                @Override
241                public DescriptionTransport apply(@NullableDecl Content content) {
242                    return content == null ? null : of(content);
243                }
244            }));
245        }
246    }
247
248    public static class UnsupportedApplicationException extends IllegalArgumentException {
249        UnsupportedApplicationException(String message) {
250            super(message);
251        }
252    }
253
254    public static class UnsupportedTransportException extends IllegalArgumentException {
255        UnsupportedTransportException(String message) {
256            super(message);
257        }
258    }
259}