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}