1package eu.siacs.conversations.xmpp.jingle;
2
3import com.google.common.base.Preconditions;
4import com.google.common.base.Strings;
5import com.google.common.collect.Collections2;
6import com.google.common.collect.ImmutableList;
7import com.google.common.collect.ImmutableMap;
8import com.google.common.collect.ImmutableSet;
9import com.google.common.collect.Iterables;
10import com.google.common.collect.Maps;
11import com.google.common.collect.Sets;
12
13import java.util.Collection;
14import java.util.List;
15import java.util.Map;
16import java.util.Set;
17
18import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
19import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
20import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
21import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
22import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
23import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
24import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
25import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
26
27public class RtpContentMap {
28
29 public final Group group;
30 public final Map<String, DescriptionTransport> contents;
31
32 public RtpContentMap(Group group, Map<String, DescriptionTransport> contents) {
33 this.group = group;
34 this.contents = contents;
35 }
36
37 public static RtpContentMap of(final JinglePacket jinglePacket) {
38 final Map<String, DescriptionTransport> contents =
39 DescriptionTransport.of(jinglePacket.getJingleContents());
40 if (isOmemoVerified(contents)) {
41 return new OmemoVerifiedRtpContentMap(jinglePacket.getGroup(), contents);
42 } else {
43 return new RtpContentMap(jinglePacket.getGroup(), contents);
44 }
45 }
46
47 private static boolean isOmemoVerified(Map<String, DescriptionTransport> contents) {
48 final Collection<DescriptionTransport> values = contents.values();
49 if (values.size() == 0) {
50 return false;
51 }
52 for (final DescriptionTransport descriptionTransport : values) {
53 if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) {
54 continue;
55 }
56 return false;
57 }
58 return true;
59 }
60
61 public static RtpContentMap of(final SessionDescription sessionDescription) {
62 final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
63 new ImmutableMap.Builder<>();
64 for (SessionDescription.Media media : sessionDescription.media) {
65 final String id = Iterables.getFirst(media.attributes.get("mid"), null);
66 Preconditions.checkNotNull(id, "media has no mid");
67 contentMapBuilder.put(id, DescriptionTransport.of(sessionDescription, media));
68 }
69 final String groupAttribute =
70 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(
77 Collections2.transform(
78 contents.values(),
79 input -> {
80 final RtpDescription rtpDescription =
81 input == null ? null : input.description;
82 return rtpDescription == null
83 ? Media.UNKNOWN
84 : input.description.getMedia();
85 }));
86 }
87
88 public List<String> getNames() {
89 return ImmutableList.copyOf(contents.keySet());
90 }
91
92 void requireContentDescriptions() {
93 if (this.contents.size() == 0) {
94 throw new IllegalStateException("No contents available");
95 }
96 for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
97 if (entry.getValue().description == null) {
98 throw new IllegalStateException(
99 String.format("%s is lacking content description", entry.getKey()));
100 }
101 }
102 }
103
104 void requireDTLSFingerprint() {
105 requireDTLSFingerprint(false);
106 }
107
108 void requireDTLSFingerprint(final boolean requireActPass) {
109 if (this.contents.size() == 0) {
110 throw new IllegalStateException("No contents available");
111 }
112 for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
113 final IceUdpTransportInfo transport = entry.getValue().transport;
114 final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
115 if (fingerprint == null
116 || Strings.isNullOrEmpty(fingerprint.getContent())
117 || Strings.isNullOrEmpty(fingerprint.getHash())) {
118 throw new SecurityException(
119 String.format(
120 "Use of DTLS-SRTP (XEP-0320) is required for content %s",
121 entry.getKey()));
122 }
123 final IceUdpTransportInfo.Setup setup = fingerprint.getSetup();
124 if (setup == null) {
125 throw new SecurityException(
126 String.format(
127 "Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute",
128 entry.getKey()));
129 }
130 if (requireActPass && setup != IceUdpTransportInfo.Setup.ACTPASS) {
131 throw new SecurityException(
132 "Initiator needs to offer ACTPASS as setup for DTLS-SRTP (XEP-0320)");
133 }
134 }
135 }
136
137 JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) {
138 final JinglePacket jinglePacket = new JinglePacket(action, sessionId);
139 if (this.group != null) {
140 jinglePacket.addGroup(this.group);
141 }
142 for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
143 final Content content = new Content(Content.Creator.INITIATOR, entry.getKey());
144 if (entry.getValue().description != null) {
145 content.addChild(entry.getValue().description);
146 }
147 content.addChild(entry.getValue().transport);
148 jinglePacket.addJingleContent(content);
149 }
150 return jinglePacket;
151 }
152
153 RtpContentMap transportInfo(
154 final String contentName, final IceUdpTransportInfo.Candidate candidate) {
155 final RtpContentMap.DescriptionTransport descriptionTransport = contents.get(contentName);
156 final IceUdpTransportInfo transportInfo =
157 descriptionTransport == null ? null : descriptionTransport.transport;
158 if (transportInfo == null) {
159 throw new IllegalArgumentException(
160 "Unable to find transport info for content name " + contentName);
161 }
162 final IceUdpTransportInfo newTransportInfo = transportInfo.cloneWrapper();
163 newTransportInfo.addChild(candidate);
164 return new RtpContentMap(
165 null,
166 ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo)));
167 }
168
169 RtpContentMap transportInfo() {
170 return new RtpContentMap(
171 null,
172 Maps.transformValues(
173 contents,
174 dt -> new DescriptionTransport(null, dt.transport.cloneWrapper())));
175 }
176
177 public IceUdpTransportInfo.Credentials getDistinctCredentials() {
178 final Set<IceUdpTransportInfo.Credentials> allCredentials = getCredentials();
179 final IceUdpTransportInfo.Credentials credentials =
180 Iterables.getFirst(allCredentials, null);
181 if (allCredentials.size() == 1 && credentials != null) {
182 if (Strings.isNullOrEmpty(credentials.password) || Strings.isNullOrEmpty(credentials.ufrag)) {
183 throw new IllegalStateException("Credentials are missing password or ufrag");
184 }
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 content -> content == null ? null : of(content)));
293 }
294 }
295
296 public static class UnsupportedApplicationException extends IllegalArgumentException {
297 UnsupportedApplicationException(String message) {
298 super(message);
299 }
300 }
301
302 public static class UnsupportedTransportException extends IllegalArgumentException {
303 UnsupportedTransportException(String message) {
304 super(message);
305 }
306 }
307}