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.Set;
 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(final SessionDescription sessionDescription) {
 65        final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
 66                new ImmutableMap.Builder<>();
 67        for (SessionDescription.Media media : sessionDescription.media) {
 68            final String id = Iterables.getFirst(media.attributes.get("mid"), null);
 69            Preconditions.checkNotNull(id, "media has no mid");
 70            contentMapBuilder.put(id, DescriptionTransport.of(sessionDescription, media));
 71        }
 72        final String groupAttribute =
 73                Iterables.getFirst(sessionDescription.attributes.get("group"), null);
 74        final Group group = groupAttribute == null ? null : Group.ofSdpString(groupAttribute);
 75        return new RtpContentMap(group, contentMapBuilder.build());
 76    }
 77
 78    public Set<Media> getMedia() {
 79        return Sets.newHashSet(
 80                Collections2.transform(
 81                        contents.values(),
 82                        input -> {
 83                            final RtpDescription rtpDescription =
 84                                    input == null ? null : input.description;
 85                            return rtpDescription == null
 86                                    ? Media.UNKNOWN
 87                                    : input.description.getMedia();
 88                        }));
 89    }
 90
 91    public List<String> getNames() {
 92        return ImmutableList.copyOf(contents.keySet());
 93    }
 94
 95    void requireContentDescriptions() {
 96        if (this.contents.size() == 0) {
 97            throw new IllegalStateException("No contents available");
 98        }
 99        for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
100            if (entry.getValue().description == null) {
101                throw new IllegalStateException(
102                        String.format("%s is lacking content description", entry.getKey()));
103            }
104        }
105    }
106
107    void requireDTLSFingerprint() {
108        requireDTLSFingerprint(false);
109    }
110
111    void requireDTLSFingerprint(final boolean requireActPass) {
112        if (this.contents.size() == 0) {
113            throw new IllegalStateException("No contents available");
114        }
115        for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
116            final IceUdpTransportInfo transport = entry.getValue().transport;
117            final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
118            if (fingerprint == null
119                    || Strings.isNullOrEmpty(fingerprint.getContent())
120                    || Strings.isNullOrEmpty(fingerprint.getHash())) {
121                throw new SecurityException(
122                        String.format(
123                                "Use of DTLS-SRTP (XEP-0320) is required for content %s",
124                                entry.getKey()));
125            }
126            final IceUdpTransportInfo.Setup setup = fingerprint.getSetup();
127            if (setup == null) {
128                throw new SecurityException(
129                        String.format(
130                                "Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute",
131                                entry.getKey()));
132            }
133            if (requireActPass && setup != IceUdpTransportInfo.Setup.ACTPASS) {
134                throw new SecurityException(
135                        "Initiator needs to offer ACTPASS as setup for DTLS-SRTP (XEP-0320)");
136            }
137        }
138    }
139
140    JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) {
141        final JinglePacket jinglePacket = new JinglePacket(action, sessionId);
142        if (this.group != null) {
143            jinglePacket.addGroup(this.group);
144        }
145        for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
146            final Content content = new Content(Content.Creator.INITIATOR, entry.getKey());
147            if (entry.getValue().description != null) {
148                content.addChild(entry.getValue().description);
149            }
150            content.addChild(entry.getValue().transport);
151            jinglePacket.addJingleContent(content);
152        }
153        return jinglePacket;
154    }
155
156    RtpContentMap transportInfo(
157            final String contentName, final IceUdpTransportInfo.Candidate candidate) {
158        final RtpContentMap.DescriptionTransport descriptionTransport = contents.get(contentName);
159        final IceUdpTransportInfo transportInfo =
160                descriptionTransport == null ? null : descriptionTransport.transport;
161        if (transportInfo == null) {
162            throw new IllegalArgumentException(
163                    "Unable to find transport info for content name " + contentName);
164        }
165        final IceUdpTransportInfo newTransportInfo = transportInfo.cloneWrapper();
166        newTransportInfo.addChild(candidate);
167        return new RtpContentMap(
168                null,
169                ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo)));
170    }
171
172    RtpContentMap transportInfo() {
173        return new RtpContentMap(
174                null,
175                Maps.transformValues(
176                        contents,
177                        dt -> new DescriptionTransport(null, dt.transport.cloneWrapper())));
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) || Strings.isNullOrEmpty(credentials.ufrag)) {
186                throw new IllegalStateException("Credentials are missing password or ufrag");
187            }
188            return credentials;
189        }
190        throw new IllegalStateException("Content map does not have distinct credentials");
191    }
192
193    public Set<IceUdpTransportInfo.Credentials> getCredentials() {
194        final Set<IceUdpTransportInfo.Credentials> credentials =
195                ImmutableSet.copyOf(
196                        Collections2.transform(
197                                contents.values(), dt -> dt.transport.getCredentials()));
198        if (credentials.isEmpty()) {
199            throw new IllegalStateException("Content map does not have any credentials");
200        }
201        return credentials;
202    }
203
204    public IceUdpTransportInfo.Credentials getCredentials(final String contentName) {
205        final DescriptionTransport descriptionTransport = this.contents.get(contentName);
206        if (descriptionTransport == null) {
207            throw new IllegalArgumentException(
208                    String.format(
209                            "Unable to find transport info for content name %s", contentName));
210        }
211        return descriptionTransport.transport.getCredentials();
212    }
213
214    public IceUdpTransportInfo.Setup getDtlsSetup() {
215        final Set<IceUdpTransportInfo.Setup> setups =
216                ImmutableSet.copyOf(
217                        Collections2.transform(
218                                contents.values(), dt -> dt.transport.getFingerprint().getSetup()));
219        final IceUdpTransportInfo.Setup setup = Iterables.getFirst(setups, null);
220        if (setups.size() == 1 && setup != null) {
221            return setup;
222        }
223        throw new IllegalStateException("Content map doesn't have distinct DTLS setup");
224    }
225
226    public boolean emptyCandidates() {
227        int count = 0;
228        for (DescriptionTransport descriptionTransport : contents.values()) {
229            count += descriptionTransport.transport.getCandidates().size();
230        }
231        return count == 0;
232    }
233
234    public RtpContentMap modifiedCredentials(
235            IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) {
236        final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
237                new ImmutableMap.Builder<>();
238        for (final Map.Entry<String, DescriptionTransport> content : contents.entrySet()) {
239            final RtpDescription rtpDescription = content.getValue().description;
240            IceUdpTransportInfo transportInfo = content.getValue().transport;
241            final IceUdpTransportInfo modifiedTransportInfo =
242                    transportInfo.modifyCredentials(credentials, setup);
243            contentMapBuilder.put(
244                    content.getKey(),
245                    new DescriptionTransport(rtpDescription, modifiedTransportInfo));
246        }
247        return new RtpContentMap(this.group, contentMapBuilder.build());
248    }
249
250    public static class DescriptionTransport {
251        public final RtpDescription description;
252        public final IceUdpTransportInfo transport;
253
254        public DescriptionTransport(
255                final RtpDescription description, final IceUdpTransportInfo transport) {
256            this.description = description;
257            this.transport = transport;
258        }
259
260        public static DescriptionTransport of(final Content content) {
261            final GenericDescription description = content.getDescription();
262            final GenericTransportInfo transportInfo = content.getTransport();
263            final RtpDescription rtpDescription;
264            final IceUdpTransportInfo iceUdpTransportInfo;
265            if (description == null) {
266                rtpDescription = null;
267            } else if (description instanceof RtpDescription) {
268                rtpDescription = (RtpDescription) description;
269            } else {
270                throw new UnsupportedApplicationException(
271                        "Content does not contain rtp description");
272            }
273            if (transportInfo instanceof IceUdpTransportInfo) {
274                iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo;
275            } else {
276                throw new UnsupportedTransportException(
277                        "Content does not contain ICE-UDP transport");
278            }
279            return new DescriptionTransport(
280                    rtpDescription, OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo));
281        }
282
283        public static DescriptionTransport of(
284                final SessionDescription sessionDescription, final SessionDescription.Media media) {
285            final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media);
286            final IceUdpTransportInfo transportInfo =
287                    IceUdpTransportInfo.of(sessionDescription, media);
288            return new DescriptionTransport(rtpDescription, transportInfo);
289        }
290
291        public static Map<String, DescriptionTransport> of(final Map<String, Content> contents) {
292            return ImmutableMap.copyOf(
293                    Maps.transformValues(
294                            contents,
295                            new Function<Content, DescriptionTransport>() {
296                                @NullableDecl
297                                @Override
298                                public DescriptionTransport apply(@NullableDecl Content content) {
299                                    return content == null ? null : of(content);
300                                }
301                            }));
302        }
303    }
304
305    public static class UnsupportedApplicationException extends IllegalArgumentException {
306        UnsupportedApplicationException(String message) {
307            super(message);
308        }
309    }
310
311    public static class UnsupportedTransportException extends IllegalArgumentException {
312        UnsupportedTransportException(String message) {
313            super(message);
314        }
315    }
316}