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.Objects;
20import java.util.Set;
21
22import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
23import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
24import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
25import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
26import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
27import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
28import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
29import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
30
31public class RtpContentMap {
32
33 public final Group group;
34 public final Map<String, DescriptionTransport> contents;
35
36 public RtpContentMap(Group group, Map<String, DescriptionTransport> contents) {
37 this.group = group;
38 this.contents = contents;
39 }
40
41 public static RtpContentMap of(final JinglePacket jinglePacket) {
42 final Map<String, DescriptionTransport> contents = 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 = new ImmutableMap.Builder<>();
66 for (SessionDescription.Media media : sessionDescription.media) {
67 final String id = Iterables.getFirst(media.attributes.get("mid"), null);
68 Preconditions.checkNotNull(id, "media has no mid");
69 contentMapBuilder.put(id, DescriptionTransport.of(sessionDescription, media));
70 }
71 final String groupAttribute = Iterables.getFirst(sessionDescription.attributes.get("group"), null);
72 final Group group = groupAttribute == null ? null : Group.ofSdpString(groupAttribute);
73 return new RtpContentMap(group, contentMapBuilder.build());
74 }
75
76 public Set<Media> getMedia() {
77 return Sets.newHashSet(Collections2.transform(contents.values(), input -> {
78 final RtpDescription rtpDescription = input == null ? null : input.description;
79 return rtpDescription == null ? Media.UNKNOWN : input.description.getMedia();
80 }));
81 }
82
83 public List<String> getNames() {
84 return ImmutableList.copyOf(contents.keySet());
85 }
86
87 void requireContentDescriptions() {
88 if (this.contents.size() == 0) {
89 throw new IllegalStateException("No contents available");
90 }
91 for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
92 if (entry.getValue().description == null) {
93 throw new IllegalStateException(String.format("%s is lacking content description", entry.getKey()));
94 }
95 }
96 }
97
98 void requireDTLSFingerprint() {
99 requireDTLSFingerprint(false);
100 }
101
102 void requireDTLSFingerprint(final boolean requireActPass) {
103 if (this.contents.size() == 0) {
104 throw new IllegalStateException("No contents available");
105 }
106 for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
107 final IceUdpTransportInfo transport = entry.getValue().transport;
108 final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
109 if (fingerprint == null || Strings.isNullOrEmpty(fingerprint.getContent()) || Strings.isNullOrEmpty(fingerprint.getHash())) {
110 throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s", entry.getKey()));
111 }
112 final IceUdpTransportInfo.Setup setup = fingerprint.getSetup();
113 if (setup == null) {
114 throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute", entry.getKey()));
115 }
116 if (requireActPass && setup != IceUdpTransportInfo.Setup.ACTPASS) {
117 throw new SecurityException("Initiator needs to offer ACTPASS as setup for DTLS-SRTP (XEP-0320)");
118 }
119 }
120 }
121
122 JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) {
123 final JinglePacket jinglePacket = new JinglePacket(action, sessionId);
124 if (this.group != null) {
125 jinglePacket.addGroup(this.group);
126 }
127 for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
128 final Content content = new Content(Content.Creator.INITIATOR, entry.getKey());
129 if (entry.getValue().description != null) {
130 content.addChild(entry.getValue().description);
131 }
132 content.addChild(entry.getValue().transport);
133 jinglePacket.addJingleContent(content);
134 }
135 return jinglePacket;
136 }
137
138 RtpContentMap transportInfo(final String contentName, final IceUdpTransportInfo.Candidate candidate) {
139 final RtpContentMap.DescriptionTransport descriptionTransport = contents.get(contentName);
140 final IceUdpTransportInfo transportInfo = descriptionTransport == null ? null : descriptionTransport.transport;
141 if (transportInfo == null) {
142 throw new IllegalArgumentException("Unable to find transport info for content name " + contentName);
143 }
144 final IceUdpTransportInfo newTransportInfo = transportInfo.cloneWrapper();
145 newTransportInfo.addChild(candidate);
146 return new RtpContentMap(null, ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo)));
147 }
148
149 RtpContentMap transportInfo() {
150 return new RtpContentMap(
151 null,
152 Maps.transformValues(contents, dt -> new DescriptionTransport(null, dt.transport.cloneWrapper()))
153 );
154 }
155
156 public IceUdpTransportInfo.Credentials getCredentials() {
157 final Set<IceUdpTransportInfo.Credentials> allCredentials = ImmutableSet.copyOf(Collections2.transform(
158 contents.values(),
159 dt -> dt.transport.getCredentials()
160 ));
161 final IceUdpTransportInfo.Credentials credentials = Iterables.getFirst(allCredentials, null);
162 if (allCredentials.size() == 1 && credentials != null) {
163 return credentials;
164 }
165 throw new IllegalStateException("Content map does not have distinct credentials");
166 }
167
168 public IceUdpTransportInfo.Setup getDtlsSetup() {
169 final Set<IceUdpTransportInfo.Setup> setups = ImmutableSet.copyOf(Collections2.transform(
170 contents.values(),
171 dt -> dt.transport.getFingerprint().getSetup()
172 ));
173 final IceUdpTransportInfo.Setup setup = Iterables.getFirst(setups, null);
174 if (setups.size() == 1 && setup != null) {
175 return setup;
176 }
177 throw new IllegalStateException("Content map doesn't have distinct DTLS setup");
178 }
179
180 public boolean emptyCandidates() {
181 int count = 0;
182 for (DescriptionTransport descriptionTransport : contents.values()) {
183 count += descriptionTransport.transport.getCandidates().size();
184 }
185 return count == 0;
186 }
187
188 public RtpContentMap modifiedCredentials(IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) {
189 final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder = new ImmutableMap.Builder<>();
190 for (final Map.Entry<String, DescriptionTransport> content : contents.entrySet()) {
191 final RtpDescription rtpDescription = content.getValue().description;
192 IceUdpTransportInfo transportInfo = content.getValue().transport;
193 final IceUdpTransportInfo modifiedTransportInfo = transportInfo.modifyCredentials(credentials, setup);
194 contentMapBuilder.put(content.getKey(), new DescriptionTransport(rtpDescription, modifiedTransportInfo));
195 }
196 return new RtpContentMap(this.group, contentMapBuilder.build());
197 }
198
199 public static class DescriptionTransport {
200 public final RtpDescription description;
201 public final IceUdpTransportInfo transport;
202
203 public DescriptionTransport(final RtpDescription description, final IceUdpTransportInfo transport) {
204 this.description = description;
205 this.transport = transport;
206 }
207
208 public static DescriptionTransport of(final Content content) {
209 final GenericDescription description = content.getDescription();
210 final GenericTransportInfo transportInfo = content.getTransport();
211 final RtpDescription rtpDescription;
212 final IceUdpTransportInfo iceUdpTransportInfo;
213 if (description == null) {
214 rtpDescription = null;
215 } else if (description instanceof RtpDescription) {
216 rtpDescription = (RtpDescription) description;
217 } else {
218 throw new UnsupportedApplicationException("Content does not contain rtp description");
219 }
220 if (transportInfo instanceof IceUdpTransportInfo) {
221 iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo;
222 } else {
223 throw new UnsupportedTransportException("Content does not contain ICE-UDP transport");
224 }
225 return new DescriptionTransport(
226 rtpDescription,
227 OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo)
228 );
229 }
230
231 public static DescriptionTransport of(final SessionDescription sessionDescription, final SessionDescription.Media media) {
232 final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media);
233 final IceUdpTransportInfo transportInfo = IceUdpTransportInfo.of(sessionDescription, media);
234 return new DescriptionTransport(rtpDescription, transportInfo);
235 }
236
237 public static Map<String, DescriptionTransport> of(final Map<String, Content> contents) {
238 return ImmutableMap.copyOf(Maps.transformValues(contents, new Function<Content, DescriptionTransport>() {
239 @NullableDecl
240 @Override
241 public DescriptionTransport apply(@NullableDecl Content content) {
242 return content == null ? null : of(content);
243 }
244 }));
245 }
246 }
247
248 public static class UnsupportedApplicationException extends IllegalArgumentException {
249 UnsupportedApplicationException(String message) {
250 super(message);
251 }
252 }
253
254 public static class UnsupportedTransportException extends IllegalArgumentException {
255 UnsupportedTransportException(String message) {
256 super(message);
257 }
258 }
259}