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.Iterables;
 10import com.google.common.collect.Maps;
 11import com.google.common.collect.Sets;
 12
 13import org.checkerframework.checker.nullness.compatqual.NullableDecl;
 14
 15import java.util.Collection;
 16import java.util.List;
 17import java.util.Map;
 18import java.util.Objects;
 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 = DescriptionTransport.of(jinglePacket.getJingleContents());
 42        if (isOmemoVerified(contents)) {
 43            return new OmemoVerifiedRtpContentMap(jinglePacket.getGroup(), contents);
 44        } else {
 45            return new RtpContentMap(jinglePacket.getGroup(), contents);
 46        }
 47    }
 48
 49    private static boolean isOmemoVerified(Map<String, DescriptionTransport> contents) {
 50        final Collection<DescriptionTransport> values = contents.values();
 51        if (values.size() == 0) {
 52            return false;
 53        }
 54        for (final DescriptionTransport descriptionTransport : values) {
 55            if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) {
 56                continue;
 57            }
 58            return false;
 59        }
 60        return true;
 61    }
 62
 63    public static RtpContentMap of(final SessionDescription sessionDescription) {
 64        final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder = new ImmutableMap.Builder<>();
 65        for (SessionDescription.Media media : sessionDescription.media) {
 66            final String id = Iterables.getFirst(media.attributes.get("mid"), null);
 67            Preconditions.checkNotNull(id, "media has no mid");
 68            contentMapBuilder.put(id, DescriptionTransport.of(sessionDescription, media));
 69        }
 70        final String groupAttribute = 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(Collections2.transform(contents.values(), input -> {
 77            final RtpDescription rtpDescription = input == null ? null : input.description;
 78            return rtpDescription == null ? Media.UNKNOWN : input.description.getMedia();
 79        }));
 80    }
 81
 82    public List<String> getNames() {
 83        return ImmutableList.copyOf(contents.keySet());
 84    }
 85
 86    void requireContentDescriptions() {
 87        if (this.contents.size() == 0) {
 88            throw new IllegalStateException("No contents available");
 89        }
 90        for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
 91            if (entry.getValue().description == null) {
 92                throw new IllegalStateException(String.format("%s is lacking content description", entry.getKey()));
 93            }
 94        }
 95    }
 96
 97    void requireDTLSFingerprint() {
 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            final IceUdpTransportInfo transport = entry.getValue().transport;
103            final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
104            if (fingerprint == null || Strings.isNullOrEmpty(fingerprint.getContent()) || Strings.isNullOrEmpty(fingerprint.getHash())) {
105                throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s", entry.getKey()));
106            }
107            if (Strings.isNullOrEmpty(fingerprint.getSetup())) {
108                throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute", entry.getKey()));
109            }
110        }
111    }
112
113    JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) {
114        final JinglePacket jinglePacket = new JinglePacket(action, sessionId);
115        if (this.group != null) {
116            jinglePacket.addGroup(this.group);
117        }
118        for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
119            final Content content = new Content(Content.Creator.INITIATOR, entry.getKey());
120            if (entry.getValue().description != null) {
121                content.addChild(entry.getValue().description);
122            }
123            content.addChild(entry.getValue().transport);
124            jinglePacket.addJingleContent(content);
125        }
126        return jinglePacket;
127    }
128
129    RtpContentMap transportInfo(final String contentName, final IceUdpTransportInfo.Candidate candidate) {
130        final RtpContentMap.DescriptionTransport descriptionTransport = contents.get(contentName);
131        final IceUdpTransportInfo transportInfo = descriptionTransport == null ? null : descriptionTransport.transport;
132        if (transportInfo == null) {
133            throw new IllegalArgumentException("Unable to find transport info for content name " + contentName);
134        }
135        final IceUdpTransportInfo newTransportInfo = transportInfo.cloneWrapper();
136        newTransportInfo.addChild(candidate);
137        return new RtpContentMap(null, ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo)));
138    }
139
140    RtpContentMap transportInfo() {
141        return new RtpContentMap(
142                null,
143                Maps.transformValues(contents, dt -> new DescriptionTransport(null, dt.transport.cloneWrapper()))
144        );
145    }
146
147    public Map<String, IceUdpTransportInfo.Credentials> getCredentials() {
148        return Maps.transformValues(contents, dt -> dt.transport.getCredentials());
149    }
150
151    public boolean emptyCandidates() {
152        int count = 0;
153        for (DescriptionTransport descriptionTransport : contents.values()) {
154            count += descriptionTransport.transport.getCandidates().size();
155        }
156        return count == 0;
157    }
158
159    public RtpContentMap modifiedCredentials(Map<String, IceUdpTransportInfo.Credentials> credentialsMap) {
160        final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder = new ImmutableMap.Builder<>();
161        for (final Map.Entry<String, DescriptionTransport> content : contents.entrySet()) {
162            final RtpDescription rtpDescription = content.getValue().description;
163            IceUdpTransportInfo transportInfo = content.getValue().transport;
164            final IceUdpTransportInfo.Credentials credentials = Objects.requireNonNull(credentialsMap.get(content.getKey()));
165            final IceUdpTransportInfo modifiedTransportInfo = transportInfo.modifyCredentials(credentials);
166            contentMapBuilder.put(content.getKey(), new DescriptionTransport(rtpDescription, modifiedTransportInfo));
167        }
168        return new RtpContentMap(this.group, contentMapBuilder.build());
169    }
170
171    public static class DescriptionTransport {
172        public final RtpDescription description;
173        public final IceUdpTransportInfo transport;
174
175        public DescriptionTransport(final RtpDescription description, final IceUdpTransportInfo transport) {
176            this.description = description;
177            this.transport = transport;
178        }
179
180        public static DescriptionTransport of(final Content content) {
181            final GenericDescription description = content.getDescription();
182            final GenericTransportInfo transportInfo = content.getTransport();
183            final RtpDescription rtpDescription;
184            final IceUdpTransportInfo iceUdpTransportInfo;
185            if (description == null) {
186                rtpDescription = null;
187            } else if (description instanceof RtpDescription) {
188                rtpDescription = (RtpDescription) description;
189            } else {
190                throw new UnsupportedApplicationException("Content does not contain rtp description");
191            }
192            if (transportInfo instanceof IceUdpTransportInfo) {
193                iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo;
194            } else {
195                throw new UnsupportedTransportException("Content does not contain ICE-UDP transport");
196            }
197            return new DescriptionTransport(
198                    rtpDescription,
199                    OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo)
200            );
201        }
202
203        public static DescriptionTransport of(final SessionDescription sessionDescription, final SessionDescription.Media media) {
204            final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media);
205            final IceUdpTransportInfo transportInfo = IceUdpTransportInfo.of(sessionDescription, media);
206            return new DescriptionTransport(rtpDescription, transportInfo);
207        }
208
209        public static Map<String, DescriptionTransport> of(final Map<String, Content> contents) {
210            return ImmutableMap.copyOf(Maps.transformValues(contents, new Function<Content, DescriptionTransport>() {
211                @NullableDecl
212                @Override
213                public DescriptionTransport apply(@NullableDecl Content content) {
214                    return content == null ? null : of(content);
215                }
216            }));
217        }
218    }
219
220    public static class UnsupportedApplicationException extends IllegalArgumentException {
221        UnsupportedApplicationException(String message) {
222            super(message);
223        }
224    }
225
226    public static class UnsupportedTransportException extends IllegalArgumentException {
227        UnsupportedTransportException(String message) {
228            super(message);
229        }
230    }
231}