1package eu.siacs.conversations.xmpp.jingle;
2
3import com.google.common.base.MoreObjects;
4import com.google.common.base.Objects;
5import com.google.common.base.Preconditions;
6import com.google.common.base.Predicates;
7import com.google.common.base.Strings;
8import com.google.common.collect.Collections2;
9import com.google.common.collect.ImmutableList;
10import com.google.common.collect.ImmutableMap;
11import com.google.common.collect.ImmutableSet;
12import com.google.common.collect.Iterables;
13import com.google.common.collect.Maps;
14import com.google.common.collect.Sets;
15
16import java.util.Collection;
17import java.util.List;
18import java.util.Map;
19import java.util.Set;
20
21import javax.annotation.Nonnull;
22
23import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
24import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
25import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
26import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
27import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
28import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
29import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
30import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
31
32public class RtpContentMap {
33
34 public final Group group;
35 public final Map<String, DescriptionTransport> contents;
36
37 public RtpContentMap(Group group, Map<String, DescriptionTransport> contents) {
38 this.group = group;
39 this.contents = contents;
40 }
41
42 public static RtpContentMap of(final JinglePacket jinglePacket) {
43 final Map<String, DescriptionTransport> contents =
44 DescriptionTransport.of(jinglePacket.getJingleContents());
45 if (isOmemoVerified(contents)) {
46 return new OmemoVerifiedRtpContentMap(jinglePacket.getGroup(), contents);
47 } else {
48 return new RtpContentMap(jinglePacket.getGroup(), contents);
49 }
50 }
51
52 private static boolean isOmemoVerified(Map<String, DescriptionTransport> contents) {
53 final Collection<DescriptionTransport> values = contents.values();
54 if (values.size() == 0) {
55 return false;
56 }
57 for (final DescriptionTransport descriptionTransport : values) {
58 if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) {
59 continue;
60 }
61 return false;
62 }
63 return true;
64 }
65
66 public static RtpContentMap of(
67 final SessionDescription sessionDescription, final boolean isInitiator) {
68 final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
69 new ImmutableMap.Builder<>();
70 for (SessionDescription.Media media : sessionDescription.media) {
71 final String id = Iterables.getFirst(media.attributes.get("mid"), null);
72 Preconditions.checkNotNull(id, "media has no mid");
73 contentMapBuilder.put(
74 id, DescriptionTransport.of(sessionDescription, isInitiator, media));
75 }
76 final String groupAttribute =
77 Iterables.getFirst(sessionDescription.attributes.get("group"), null);
78 final Group group = groupAttribute == null ? null : Group.ofSdpString(groupAttribute);
79 return new RtpContentMap(group, contentMapBuilder.build());
80 }
81
82 public Set<Media> getMedia() {
83 return Sets.newHashSet(
84 Collections2.transform(
85 contents.values(),
86 input -> {
87 final RtpDescription rtpDescription =
88 input == null ? null : input.description;
89 return rtpDescription == null
90 ? Media.UNKNOWN
91 : input.description.getMedia();
92 }));
93 }
94
95 public List<String> getNames() {
96 return ImmutableList.copyOf(contents.keySet());
97 }
98
99 void requireContentDescriptions() {
100 if (this.contents.size() == 0) {
101 throw new IllegalStateException("No contents available");
102 }
103 for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
104 if (entry.getValue().description == null) {
105 throw new IllegalStateException(
106 String.format("%s is lacking content description", entry.getKey()));
107 }
108 }
109 }
110
111 void requireDTLSFingerprint() {
112 requireDTLSFingerprint(false);
113 }
114
115 void requireDTLSFingerprint(final boolean requireActPass) {
116 if (this.contents.size() == 0) {
117 throw new IllegalStateException("No contents available");
118 }
119 for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
120 final IceUdpTransportInfo transport = entry.getValue().transport;
121 final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
122 if (fingerprint == null
123 || Strings.isNullOrEmpty(fingerprint.getContent())
124 || Strings.isNullOrEmpty(fingerprint.getHash())) {
125 throw new SecurityException(
126 String.format(
127 "Use of DTLS-SRTP (XEP-0320) is required for content %s",
128 entry.getKey()));
129 }
130 final IceUdpTransportInfo.Setup setup = fingerprint.getSetup();
131 if (setup == null) {
132 throw new SecurityException(
133 String.format(
134 "Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute",
135 entry.getKey()));
136 }
137 if (requireActPass && setup != IceUdpTransportInfo.Setup.ACTPASS) {
138 throw new SecurityException(
139 "Initiator needs to offer ACTPASS as setup for DTLS-SRTP (XEP-0320)");
140 }
141 }
142 }
143
144 JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) {
145 final JinglePacket jinglePacket = new JinglePacket(action, sessionId);
146 if (this.group != null) {
147 jinglePacket.addGroup(this.group);
148 }
149 for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
150 final DescriptionTransport descriptionTransport = entry.getValue();
151 final Content content =
152 new Content(
153 Content.Creator.INITIATOR,
154 descriptionTransport.senders,
155 entry.getKey());
156 if (descriptionTransport.description != null) {
157 content.addChild(descriptionTransport.description);
158 }
159 content.addChild(descriptionTransport.transport);
160 jinglePacket.addJingleContent(content);
161 }
162 return jinglePacket;
163 }
164
165 RtpContentMap transportInfo(
166 final String contentName, final IceUdpTransportInfo.Candidate candidate) {
167 final RtpContentMap.DescriptionTransport descriptionTransport = contents.get(contentName);
168 final IceUdpTransportInfo transportInfo =
169 descriptionTransport == null ? null : descriptionTransport.transport;
170 if (transportInfo == null) {
171 throw new IllegalArgumentException(
172 "Unable to find transport info for content name " + contentName);
173 }
174 final IceUdpTransportInfo newTransportInfo = transportInfo.cloneWrapper();
175 newTransportInfo.addChild(candidate);
176 return new RtpContentMap(
177 null,
178 ImmutableMap.of(
179 contentName,
180 new DescriptionTransport(
181 descriptionTransport.senders, null, newTransportInfo)));
182 }
183
184 RtpContentMap transportInfo() {
185 return new RtpContentMap(
186 null,
187 Maps.transformValues(
188 contents,
189 dt ->
190 new DescriptionTransport(
191 dt.senders, null, dt.transport.cloneWrapper())));
192 }
193
194 public IceUdpTransportInfo.Credentials getDistinctCredentials() {
195 final Set<IceUdpTransportInfo.Credentials> allCredentials = getCredentials();
196 final IceUdpTransportInfo.Credentials credentials =
197 Iterables.getFirst(allCredentials, null);
198 if (allCredentials.size() == 1 && credentials != null) {
199 if (Strings.isNullOrEmpty(credentials.password)
200 || Strings.isNullOrEmpty(credentials.ufrag)) {
201 throw new IllegalStateException("Credentials are missing password or ufrag");
202 }
203 return credentials;
204 }
205 throw new IllegalStateException("Content map does not have distinct credentials");
206 }
207
208 public Set<IceUdpTransportInfo.Credentials> getCredentials() {
209 final Set<IceUdpTransportInfo.Credentials> credentials =
210 ImmutableSet.copyOf(
211 Collections2.transform(
212 contents.values(), dt -> dt.transport.getCredentials()));
213 if (credentials.isEmpty()) {
214 throw new IllegalStateException("Content map does not have any credentials");
215 }
216 return credentials;
217 }
218
219 public IceUdpTransportInfo.Credentials getCredentials(final String contentName) {
220 final DescriptionTransport descriptionTransport = this.contents.get(contentName);
221 if (descriptionTransport == null) {
222 throw new IllegalArgumentException(
223 String.format(
224 "Unable to find transport info for content name %s", contentName));
225 }
226 return descriptionTransport.transport.getCredentials();
227 }
228
229 public IceUdpTransportInfo.Setup getDtlsSetup() {
230 final Set<IceUdpTransportInfo.Setup> setups =
231 ImmutableSet.copyOf(
232 Collections2.transform(
233 contents.values(), dt -> dt.transport.getFingerprint().getSetup()));
234 final IceUdpTransportInfo.Setup setup = Iterables.getFirst(setups, null);
235 if (setups.size() == 1 && setup != null) {
236 return setup;
237 }
238 throw new IllegalStateException("Content map doesn't have distinct DTLS setup");
239 }
240
241 private DTLS getDistinctDtls() {
242 final Set<DTLS> dtlsSet =
243 ImmutableSet.copyOf(
244 Collections2.transform(
245 contents.values(),
246 dt -> {
247 final IceUdpTransportInfo.Fingerprint fp =
248 dt.transport.getFingerprint();
249 return new DTLS(fp.getHash(), fp.getSetup(), fp.getContent());
250 }));
251 final DTLS dtls = Iterables.getFirst(dtlsSet, null);
252 if (dtlsSet.size() == 1 && dtls != null) {
253 return dtls;
254 }
255 throw new IllegalStateException("Content map doesn't have distinct DTLS setup");
256 }
257
258 public boolean emptyCandidates() {
259 int count = 0;
260 for (DescriptionTransport descriptionTransport : contents.values()) {
261 count += descriptionTransport.transport.getCandidates().size();
262 }
263 return count == 0;
264 }
265
266 public RtpContentMap modifiedCredentials(
267 IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) {
268 final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
269 new ImmutableMap.Builder<>();
270 for (final Map.Entry<String, DescriptionTransport> content : contents.entrySet()) {
271 final DescriptionTransport descriptionTransport = content.getValue();
272 final RtpDescription rtpDescription = descriptionTransport.description;
273 final IceUdpTransportInfo transportInfo = descriptionTransport.transport;
274 final IceUdpTransportInfo modifiedTransportInfo =
275 transportInfo.modifyCredentials(credentials, setup);
276 contentMapBuilder.put(
277 content.getKey(),
278 new DescriptionTransport(
279 descriptionTransport.senders, rtpDescription, modifiedTransportInfo));
280 }
281 return new RtpContentMap(this.group, contentMapBuilder.build());
282 }
283
284 public RtpContentMap toContentModification(final Collection<String> modifications) {
285 return new RtpContentMap(
286 this.group,
287 Maps.transformValues(
288 Maps.filterKeys(contents, Predicates.in(modifications)),
289 dt ->
290 new DescriptionTransport(
291 dt.senders, dt.description, IceUdpTransportInfo.STUB)));
292 }
293
294 public Diff diff(final RtpContentMap rtpContentMap) {
295 final Set<String> existingContentIds = this.contents.keySet();
296 final Set<String> newContentIds = rtpContentMap.contents.keySet();
297 return new Diff(
298 ImmutableSet.copyOf(Sets.difference(newContentIds, existingContentIds)),
299 ImmutableSet.copyOf(Sets.difference(existingContentIds, newContentIds)));
300 }
301
302 public boolean iceRestart(final RtpContentMap rtpContentMap) {
303 try {
304 return !getDistinctCredentials().equals(rtpContentMap.getDistinctCredentials());
305 } catch (final IllegalStateException e) {
306 return false;
307 }
308 }
309
310 public RtpContentMap addContent(final RtpContentMap modification) {
311 final IceUdpTransportInfo.Credentials credentials = getDistinctCredentials();
312 final DTLS dtls = getDistinctDtls();
313 final IceUdpTransportInfo iceUdpTransportInfo =
314 IceUdpTransportInfo.of(credentials, dtls.setup, dtls.hash, dtls.fingerprint);
315 final Map<String, DescriptionTransport> combined =
316 new ImmutableMap.Builder<String, DescriptionTransport>()
317 .putAll(contents)
318 .putAll(
319 Maps.transformValues(
320 modification.contents,
321 dt ->
322 new DescriptionTransport(
323 dt.senders,
324 dt.description,
325 iceUdpTransportInfo)))
326 .build();
327 return new RtpContentMap(modification.group, combined);
328 }
329
330 public static class DescriptionTransport {
331 public final Content.Senders senders;
332 public final RtpDescription description;
333 public final IceUdpTransportInfo transport;
334
335 public DescriptionTransport(
336 final Content.Senders senders,
337 final RtpDescription description,
338 final IceUdpTransportInfo transport) {
339 this.senders = senders;
340 this.description = description;
341 this.transport = transport;
342 }
343
344 public static DescriptionTransport of(final Content content) {
345 final GenericDescription description = content.getDescription();
346 final GenericTransportInfo transportInfo = content.getTransport();
347 final Content.Senders senders = content.getSenders();
348 final RtpDescription rtpDescription;
349 final IceUdpTransportInfo iceUdpTransportInfo;
350 if (description == null) {
351 rtpDescription = null;
352 } else if (description instanceof RtpDescription) {
353 rtpDescription = (RtpDescription) description;
354 } else {
355 throw new UnsupportedApplicationException(
356 "Content does not contain rtp description");
357 }
358 if (transportInfo instanceof IceUdpTransportInfo) {
359 iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo;
360 } else {
361 throw new UnsupportedTransportException(
362 "Content does not contain ICE-UDP transport");
363 }
364 return new DescriptionTransport(
365 senders,
366 rtpDescription,
367 OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo));
368 }
369
370 private static DescriptionTransport of(
371 final SessionDescription sessionDescription,
372 final boolean isInitiator,
373 final SessionDescription.Media media) {
374 final Content.Senders senders = Content.Senders.of(media, isInitiator);
375 final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media);
376 final IceUdpTransportInfo transportInfo =
377 IceUdpTransportInfo.of(sessionDescription, media);
378 return new DescriptionTransport(senders, rtpDescription, transportInfo);
379 }
380
381 public static Map<String, DescriptionTransport> of(final Map<String, Content> contents) {
382 return ImmutableMap.copyOf(
383 Maps.transformValues(
384 contents, content -> content == null ? null : of(content)));
385 }
386 }
387
388 public static class UnsupportedApplicationException extends IllegalArgumentException {
389 UnsupportedApplicationException(String message) {
390 super(message);
391 }
392 }
393
394 public static class UnsupportedTransportException extends IllegalArgumentException {
395 UnsupportedTransportException(String message) {
396 super(message);
397 }
398 }
399
400 public static final class Diff {
401 public final Set<String> added;
402 public final Set<String> removed;
403
404 private Diff(final Set<String> added, final Set<String> removed) {
405 this.added = added;
406 this.removed = removed;
407 }
408
409 public boolean hasModifications() {
410 return !this.added.isEmpty() || !this.removed.isEmpty();
411 }
412
413 @Override
414 @Nonnull
415 public String toString() {
416 return MoreObjects.toStringHelper(this)
417 .add("added", added)
418 .add("removed", removed)
419 .toString();
420 }
421 }
422
423 public static final class DTLS {
424 public final String hash;
425 public final IceUdpTransportInfo.Setup setup;
426 public final String fingerprint;
427
428 private DTLS(String hash, IceUdpTransportInfo.Setup setup, String fingerprint) {
429 this.hash = hash;
430 this.setup = setup;
431 this.fingerprint = fingerprint;
432 }
433
434 @Override
435 public boolean equals(Object o) {
436 if (this == o) return true;
437 if (o == null || getClass() != o.getClass()) return false;
438 DTLS dtls = (DTLS) o;
439 return Objects.equal(hash, dtls.hash)
440 && setup == dtls.setup
441 && Objects.equal(fingerprint, dtls.fingerprint);
442 }
443
444 @Override
445 public int hashCode() {
446 return Objects.hashCode(hash, setup, fingerprint);
447 }
448 }
449}