1package eu.siacs.conversations.xmpp.jingle;
2
3import com.google.common.base.MoreObjects;
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 java.util.Collection;
15import java.util.List;
16import java.util.Map;
17import java.util.Set;
18
19import javax.annotation.Nonnull;
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(
65 final SessionDescription sessionDescription, final boolean isInitiator) {
66 final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
67 new ImmutableMap.Builder<>();
68 for (SessionDescription.Media media : sessionDescription.media) {
69 final String id = Iterables.getFirst(media.attributes.get("mid"), null);
70 Preconditions.checkNotNull(id, "media has no mid");
71 contentMapBuilder.put(
72 id, DescriptionTransport.of(sessionDescription, isInitiator, media));
73 }
74 final String groupAttribute =
75 Iterables.getFirst(sessionDescription.attributes.get("group"), null);
76 final Group group = groupAttribute == null ? null : Group.ofSdpString(groupAttribute);
77 return new RtpContentMap(group, contentMapBuilder.build());
78 }
79
80 public Set<Media> getMedia() {
81 return Sets.newHashSet(
82 Collections2.transform(
83 contents.values(),
84 input -> {
85 final RtpDescription rtpDescription =
86 input == null ? null : input.description;
87 return rtpDescription == null
88 ? Media.UNKNOWN
89 : input.description.getMedia();
90 }));
91 }
92
93 public List<String> getNames() {
94 return ImmutableList.copyOf(contents.keySet());
95 }
96
97 void requireContentDescriptions() {
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 if (entry.getValue().description == null) {
103 throw new IllegalStateException(
104 String.format("%s is lacking content description", entry.getKey()));
105 }
106 }
107 }
108
109 void requireDTLSFingerprint() {
110 requireDTLSFingerprint(false);
111 }
112
113 void requireDTLSFingerprint(final boolean requireActPass) {
114 if (this.contents.size() == 0) {
115 throw new IllegalStateException("No contents available");
116 }
117 for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
118 final IceUdpTransportInfo transport = entry.getValue().transport;
119 final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
120 if (fingerprint == null
121 || Strings.isNullOrEmpty(fingerprint.getContent())
122 || Strings.isNullOrEmpty(fingerprint.getHash())) {
123 throw new SecurityException(
124 String.format(
125 "Use of DTLS-SRTP (XEP-0320) is required for content %s",
126 entry.getKey()));
127 }
128 final IceUdpTransportInfo.Setup setup = fingerprint.getSetup();
129 if (setup == null) {
130 throw new SecurityException(
131 String.format(
132 "Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute",
133 entry.getKey()));
134 }
135 if (requireActPass && setup != IceUdpTransportInfo.Setup.ACTPASS) {
136 throw new SecurityException(
137 "Initiator needs to offer ACTPASS as setup for DTLS-SRTP (XEP-0320)");
138 }
139 }
140 }
141
142 JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) {
143 final JinglePacket jinglePacket = new JinglePacket(action, sessionId);
144 if (this.group != null) {
145 jinglePacket.addGroup(this.group);
146 }
147 for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
148 final DescriptionTransport descriptionTransport = entry.getValue();
149 final Content content =
150 new Content(
151 Content.Creator.INITIATOR,
152 descriptionTransport.senders,
153 entry.getKey());
154 if (descriptionTransport.description != null) {
155 content.addChild(descriptionTransport.description);
156 }
157 content.addChild(descriptionTransport.transport);
158 jinglePacket.addJingleContent(content);
159 }
160 return jinglePacket;
161 }
162
163 RtpContentMap transportInfo(
164 final String contentName, final IceUdpTransportInfo.Candidate candidate) {
165 final RtpContentMap.DescriptionTransport descriptionTransport = contents.get(contentName);
166 final IceUdpTransportInfo transportInfo =
167 descriptionTransport == null ? null : descriptionTransport.transport;
168 if (transportInfo == null) {
169 throw new IllegalArgumentException(
170 "Unable to find transport info for content name " + contentName);
171 }
172 final IceUdpTransportInfo newTransportInfo = transportInfo.cloneWrapper();
173 newTransportInfo.addChild(candidate);
174 return new RtpContentMap(
175 null,
176 ImmutableMap.of(
177 contentName,
178 new DescriptionTransport(
179 descriptionTransport.senders, null, newTransportInfo)));
180 }
181
182 RtpContentMap transportInfo() {
183 return new RtpContentMap(
184 null,
185 Maps.transformValues(
186 contents,
187 dt ->
188 new DescriptionTransport(
189 dt.senders, null, dt.transport.cloneWrapper())));
190 }
191
192 public IceUdpTransportInfo.Credentials getDistinctCredentials() {
193 final Set<IceUdpTransportInfo.Credentials> allCredentials = getCredentials();
194 final IceUdpTransportInfo.Credentials credentials =
195 Iterables.getFirst(allCredentials, null);
196 if (allCredentials.size() == 1 && credentials != null) {
197 if (Strings.isNullOrEmpty(credentials.password)
198 || Strings.isNullOrEmpty(credentials.ufrag)) {
199 throw new IllegalStateException("Credentials are missing password or ufrag");
200 }
201 return credentials;
202 }
203 throw new IllegalStateException("Content map does not have distinct credentials");
204 }
205
206 public Set<IceUdpTransportInfo.Credentials> getCredentials() {
207 final Set<IceUdpTransportInfo.Credentials> credentials =
208 ImmutableSet.copyOf(
209 Collections2.transform(
210 contents.values(), dt -> dt.transport.getCredentials()));
211 if (credentials.isEmpty()) {
212 throw new IllegalStateException("Content map does not have any credentials");
213 }
214 return credentials;
215 }
216
217 public IceUdpTransportInfo.Credentials getCredentials(final String contentName) {
218 final DescriptionTransport descriptionTransport = this.contents.get(contentName);
219 if (descriptionTransport == null) {
220 throw new IllegalArgumentException(
221 String.format(
222 "Unable to find transport info for content name %s", contentName));
223 }
224 return descriptionTransport.transport.getCredentials();
225 }
226
227 public IceUdpTransportInfo.Setup getDtlsSetup() {
228 final Set<IceUdpTransportInfo.Setup> setups =
229 ImmutableSet.copyOf(
230 Collections2.transform(
231 contents.values(), dt -> dt.transport.getFingerprint().getSetup()));
232 final IceUdpTransportInfo.Setup setup = Iterables.getFirst(setups, null);
233 if (setups.size() == 1 && setup != null) {
234 return setup;
235 }
236 throw new IllegalStateException("Content map doesn't have distinct DTLS setup");
237 }
238
239 public boolean emptyCandidates() {
240 int count = 0;
241 for (DescriptionTransport descriptionTransport : contents.values()) {
242 count += descriptionTransport.transport.getCandidates().size();
243 }
244 return count == 0;
245 }
246
247 public RtpContentMap modifiedCredentials(
248 IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) {
249 final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
250 new ImmutableMap.Builder<>();
251 for (final Map.Entry<String, DescriptionTransport> content : contents.entrySet()) {
252 final DescriptionTransport descriptionTransport = content.getValue();
253 final RtpDescription rtpDescription = descriptionTransport.description;
254 final IceUdpTransportInfo transportInfo = descriptionTransport.transport;
255 final IceUdpTransportInfo modifiedTransportInfo =
256 transportInfo.modifyCredentials(credentials, setup);
257 contentMapBuilder.put(
258 content.getKey(),
259 new DescriptionTransport(
260 descriptionTransport.senders, rtpDescription, modifiedTransportInfo));
261 }
262 return new RtpContentMap(this.group, contentMapBuilder.build());
263 }
264
265 public Diff diff(final RtpContentMap rtpContentMap) {
266 final Set<String> existingContentIds = this.contents.keySet();
267 final Set<String> newContentIds = rtpContentMap.contents.keySet();
268 return new Diff(
269 Sets.difference(newContentIds, existingContentIds),
270 Sets.difference(existingContentIds, newContentIds));
271 }
272
273 public boolean iceRestart(final RtpContentMap rtpContentMap) {
274 try {
275 return !getDistinctCredentials().equals(rtpContentMap.getDistinctCredentials());
276 } catch (final IllegalStateException e) {
277 return false;
278 }
279 }
280
281 public static class DescriptionTransport {
282 public final Content.Senders senders;
283 public final RtpDescription description;
284 public final IceUdpTransportInfo transport;
285
286 public DescriptionTransport(
287 final Content.Senders senders,
288 final RtpDescription description,
289 final IceUdpTransportInfo transport) {
290 this.senders = senders;
291 this.description = description;
292 this.transport = transport;
293 }
294
295 public static DescriptionTransport of(final Content content) {
296 final GenericDescription description = content.getDescription();
297 final GenericTransportInfo transportInfo = content.getTransport();
298 final Content.Senders senders = content.getSenders();
299 final RtpDescription rtpDescription;
300 final IceUdpTransportInfo iceUdpTransportInfo;
301 if (description == null) {
302 rtpDescription = null;
303 } else if (description instanceof RtpDescription) {
304 rtpDescription = (RtpDescription) description;
305 } else {
306 throw new UnsupportedApplicationException(
307 "Content does not contain rtp description");
308 }
309 if (transportInfo instanceof IceUdpTransportInfo) {
310 iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo;
311 } else {
312 throw new UnsupportedTransportException(
313 "Content does not contain ICE-UDP transport");
314 }
315 return new DescriptionTransport(
316 senders,
317 rtpDescription,
318 OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo));
319 }
320
321 private static DescriptionTransport of(
322 final SessionDescription sessionDescription,
323 final boolean isInitiator,
324 final SessionDescription.Media media) {
325 final Content.Senders senders = Content.Senders.of(media, isInitiator);
326 final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media);
327 final IceUdpTransportInfo transportInfo =
328 IceUdpTransportInfo.of(sessionDescription, media);
329 return new DescriptionTransport(senders, rtpDescription, transportInfo);
330 }
331
332 public static Map<String, DescriptionTransport> of(final Map<String, Content> contents) {
333 return ImmutableMap.copyOf(
334 Maps.transformValues(
335 contents, content -> content == null ? null : of(content)));
336 }
337 }
338
339 public static class UnsupportedApplicationException extends IllegalArgumentException {
340 UnsupportedApplicationException(String message) {
341 super(message);
342 }
343 }
344
345 public static class UnsupportedTransportException extends IllegalArgumentException {
346 UnsupportedTransportException(String message) {
347 super(message);
348 }
349 }
350
351 public static final class Diff {
352 public final Set<String> added;
353 public final Set<String> removed;
354
355 private Diff(final Set<String> added, final Set<String> removed) {
356 this.added = added;
357 this.removed = removed;
358 }
359
360 public boolean hasModifications() {
361 return !this.added.isEmpty() || !this.removed.isEmpty();
362 }
363
364 @Override
365 @Nonnull
366 public String toString() {
367 return MoreObjects.toStringHelper(this)
368 .add("added", added)
369 .add("removed", removed)
370 .toString();
371 }
372 }
373}