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 if (Strings.isNullOrEmpty(credentials.password) || Strings.isNullOrEmpty(credentials.ufrag)) {
186 throw new IllegalStateException("Credentials are missing password or ufrag");
187 }
188 return credentials;
189 }
190 throw new IllegalStateException("Content map does not have distinct credentials");
191 }
192
193 public Set<IceUdpTransportInfo.Credentials> getCredentials() {
194 final Set<IceUdpTransportInfo.Credentials> credentials =
195 ImmutableSet.copyOf(
196 Collections2.transform(
197 contents.values(), dt -> dt.transport.getCredentials()));
198 if (credentials.isEmpty()) {
199 throw new IllegalStateException("Content map does not have any credentials");
200 }
201 return credentials;
202 }
203
204 public IceUdpTransportInfo.Credentials getCredentials(final String contentName) {
205 final DescriptionTransport descriptionTransport = this.contents.get(contentName);
206 if (descriptionTransport == null) {
207 throw new IllegalArgumentException(
208 String.format(
209 "Unable to find transport info for content name %s", contentName));
210 }
211 return descriptionTransport.transport.getCredentials();
212 }
213
214 public IceUdpTransportInfo.Setup getDtlsSetup() {
215 final Set<IceUdpTransportInfo.Setup> setups =
216 ImmutableSet.copyOf(
217 Collections2.transform(
218 contents.values(), dt -> dt.transport.getFingerprint().getSetup()));
219 final IceUdpTransportInfo.Setup setup = Iterables.getFirst(setups, null);
220 if (setups.size() == 1 && setup != null) {
221 return setup;
222 }
223 throw new IllegalStateException("Content map doesn't have distinct DTLS setup");
224 }
225
226 public boolean emptyCandidates() {
227 int count = 0;
228 for (DescriptionTransport descriptionTransport : contents.values()) {
229 count += descriptionTransport.transport.getCandidates().size();
230 }
231 return count == 0;
232 }
233
234 public RtpContentMap modifiedCredentials(
235 IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) {
236 final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
237 new ImmutableMap.Builder<>();
238 for (final Map.Entry<String, DescriptionTransport> content : contents.entrySet()) {
239 final RtpDescription rtpDescription = content.getValue().description;
240 IceUdpTransportInfo transportInfo = content.getValue().transport;
241 final IceUdpTransportInfo modifiedTransportInfo =
242 transportInfo.modifyCredentials(credentials, setup);
243 contentMapBuilder.put(
244 content.getKey(),
245 new DescriptionTransport(rtpDescription, modifiedTransportInfo));
246 }
247 return new RtpContentMap(this.group, contentMapBuilder.build());
248 }
249
250 public static class DescriptionTransport {
251 public final RtpDescription description;
252 public final IceUdpTransportInfo transport;
253
254 public DescriptionTransport(
255 final RtpDescription description, final IceUdpTransportInfo transport) {
256 this.description = description;
257 this.transport = transport;
258 }
259
260 public static DescriptionTransport of(final Content content) {
261 final GenericDescription description = content.getDescription();
262 final GenericTransportInfo transportInfo = content.getTransport();
263 final RtpDescription rtpDescription;
264 final IceUdpTransportInfo iceUdpTransportInfo;
265 if (description == null) {
266 rtpDescription = null;
267 } else if (description instanceof RtpDescription) {
268 rtpDescription = (RtpDescription) description;
269 } else {
270 throw new UnsupportedApplicationException(
271 "Content does not contain rtp description");
272 }
273 if (transportInfo instanceof IceUdpTransportInfo) {
274 iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo;
275 } else {
276 throw new UnsupportedTransportException(
277 "Content does not contain ICE-UDP transport");
278 }
279 return new DescriptionTransport(
280 rtpDescription, OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo));
281 }
282
283 public static DescriptionTransport of(
284 final SessionDescription sessionDescription, final SessionDescription.Media media) {
285 final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media);
286 final IceUdpTransportInfo transportInfo =
287 IceUdpTransportInfo.of(sessionDescription, media);
288 return new DescriptionTransport(rtpDescription, transportInfo);
289 }
290
291 public static Map<String, DescriptionTransport> of(final Map<String, Content> contents) {
292 return ImmutableMap.copyOf(
293 Maps.transformValues(
294 contents,
295 new Function<Content, DescriptionTransport>() {
296 @NullableDecl
297 @Override
298 public DescriptionTransport apply(@NullableDecl Content content) {
299 return content == null ? null : of(content);
300 }
301 }));
302 }
303 }
304
305 public static class UnsupportedApplicationException extends IllegalArgumentException {
306 UnsupportedApplicationException(String message) {
307 super(message);
308 }
309 }
310
311 public static class UnsupportedTransportException extends IllegalArgumentException {
312 UnsupportedTransportException(String message) {
313 super(message);
314 }
315 }
316}