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 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 new Function<Content, DescriptionTransport>() {
293 @NullableDecl
294 @Override
295 public DescriptionTransport apply(@NullableDecl Content content) {
296 return content == null ? null : of(content);
297 }
298 }));
299 }
300 }
301
302 public static class UnsupportedApplicationException extends IllegalArgumentException {
303 UnsupportedApplicationException(String message) {
304 super(message);
305 }
306 }
307
308 public static class UnsupportedTransportException extends IllegalArgumentException {
309 UnsupportedTransportException(String message) {
310 super(message);
311 }
312 }
313}