RtpContentMap.java

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