1package eu.siacs.conversations.xmpp.jingle.stanzas;
2
3import android.util.Log;
4
5import androidx.annotation.NonNull;
6
7import com.google.common.base.Joiner;
8import com.google.common.base.MoreObjects;
9import com.google.common.base.Objects;
10import com.google.common.base.Preconditions;
11import com.google.common.base.Splitter;
12import com.google.common.base.Strings;
13import com.google.common.collect.ArrayListMultimap;
14import com.google.common.collect.Collections2;
15import com.google.common.collect.ImmutableList;
16import com.google.common.collect.Iterables;
17
18import java.util.Arrays;
19import java.util.Collection;
20import java.util.Collections;
21import java.util.HashMap;
22import java.util.Hashtable;
23import java.util.LinkedHashMap;
24import java.util.List;
25import java.util.Locale;
26import java.util.Map;
27import java.util.UUID;
28
29import eu.siacs.conversations.Config;
30import eu.siacs.conversations.xml.Element;
31import eu.siacs.conversations.xml.Namespace;
32import eu.siacs.conversations.xmpp.jingle.SessionDescription;
33
34public class IceUdpTransportInfo extends GenericTransportInfo {
35
36 public static final IceUdpTransportInfo STUB = new IceUdpTransportInfo();
37
38 public IceUdpTransportInfo() {
39 super("transport", Namespace.JINGLE_TRANSPORT_ICE_UDP);
40 }
41
42 public static IceUdpTransportInfo upgrade(final Element element) {
43 Preconditions.checkArgument(
44 "transport".equals(element.getName()), "Name of provided element is not transport");
45 Preconditions.checkArgument(
46 Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(element.getNamespace()),
47 "Element does not match ice-udp transport namespace");
48 final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo();
49 transportInfo.setAttributes(element.getAttributes());
50 transportInfo.setChildren(element.getChildren());
51 return transportInfo;
52 }
53
54 public static IceUdpTransportInfo of(
55 SessionDescription sessionDescription, SessionDescription.Media media) {
56 final String ufrag = Iterables.getFirst(media.attributes.get("ice-ufrag"), null);
57 final String pwd = Iterables.getFirst(media.attributes.get("ice-pwd"), null);
58 final IceUdpTransportInfo iceUdpTransportInfo = new IceUdpTransportInfo();
59 if (ufrag != null) {
60 iceUdpTransportInfo.setAttribute("ufrag", ufrag);
61 }
62 if (pwd != null) {
63 iceUdpTransportInfo.setAttribute("pwd", pwd);
64 }
65 final Fingerprint fingerprint = Fingerprint.of(sessionDescription, media);
66 if (fingerprint != null) {
67 iceUdpTransportInfo.addChild(fingerprint);
68 }
69 for (final String iceOption : IceOption.of(media)) {
70 iceUdpTransportInfo.addChild(new IceOption(iceOption));
71 }
72 return iceUdpTransportInfo;
73 }
74
75 public static IceUdpTransportInfo of(
76 final Credentials credentials, final Setup setup, final String hash, final String fingerprint) {
77 final IceUdpTransportInfo iceUdpTransportInfo = new IceUdpTransportInfo();
78 iceUdpTransportInfo.addChild(Fingerprint.of(setup, hash, fingerprint));
79 iceUdpTransportInfo.setAttribute("ufrag", credentials.ufrag);
80 iceUdpTransportInfo.setAttribute("pwd", credentials.password);
81 return iceUdpTransportInfo;
82 }
83
84 public Fingerprint getFingerprint() {
85 final Element fingerprint = this.findChild("fingerprint", Namespace.JINGLE_APPS_DTLS);
86 return fingerprint == null ? null : Fingerprint.upgrade(fingerprint);
87 }
88
89 public List<String> getIceOptions() {
90 final ImmutableList.Builder<String> optionBuilder = new ImmutableList.Builder<>();
91 for(final Element child : this.children) {
92 if (Namespace.JINGLE_TRANSPORT_ICE_OPTION.equals(child.getNamespace()) && IceOption.WELL_KNOWN.contains(child.getName())) {
93 optionBuilder.add(child.getName());
94 }
95 }
96 return optionBuilder.build();
97 }
98
99 public Credentials getCredentials() {
100 final String ufrag = this.getAttribute("ufrag");
101 final String password = this.getAttribute("pwd");
102 return new Credentials(ufrag, password);
103 }
104
105 public List<Candidate> getCandidates() {
106 final ImmutableList.Builder<Candidate> builder = new ImmutableList.Builder<>();
107 for (final Element child : getChildren()) {
108 if ("candidate".equals(child.getName())) {
109 builder.add(Candidate.upgrade(child));
110 }
111 }
112 return builder.build();
113 }
114
115 public IceUdpTransportInfo cloneWrapper() {
116 final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo();
117 transportInfo.setAttributes(new Hashtable<>(getAttributes()));
118 return transportInfo;
119 }
120
121 public IceUdpTransportInfo modifyCredentials(final Credentials credentials, final Setup setup) {
122 final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo();
123 transportInfo.setAttribute("ufrag", credentials.ufrag);
124 transportInfo.setAttribute("pwd", credentials.password);
125 for (final Element child : getChildren()) {
126 if (child.getName().equals("fingerprint")
127 && Namespace.JINGLE_APPS_DTLS.equals(child.getNamespace())) {
128 final Fingerprint fingerprint = new Fingerprint();
129 fingerprint.setAttributes(new Hashtable<>(child.getAttributes()));
130 fingerprint.setContent(child.getContent());
131 fingerprint.setAttribute("setup", setup.toString().toLowerCase(Locale.ROOT));
132 transportInfo.addChild(fingerprint);
133 }
134 }
135 return transportInfo;
136 }
137
138 public static class Credentials {
139 public final String ufrag;
140 public final String password;
141
142 public Credentials(String ufrag, String password) {
143 this.ufrag = ufrag;
144 this.password = password;
145 }
146
147 @Override
148 public boolean equals(Object o) {
149 if (this == o) return true;
150 if (o == null || getClass() != o.getClass()) return false;
151 Credentials that = (Credentials) o;
152 return Objects.equal(ufrag, that.ufrag) && Objects.equal(password, that.password);
153 }
154
155 @Override
156 public int hashCode() {
157 return Objects.hashCode(ufrag, password);
158 }
159
160 @Override
161 @NonNull
162 public String toString() {
163 return MoreObjects.toStringHelper(this)
164 .add("ufrag", ufrag)
165 .add("password", password)
166 .toString();
167 }
168 }
169
170 public static class Candidate extends Element {
171
172 private Candidate() {
173 super("candidate");
174 }
175
176 public static Candidate upgrade(final Element element) {
177 Preconditions.checkArgument("candidate".equals(element.getName()));
178 final Candidate candidate = new Candidate();
179 candidate.setAttributes(element.getAttributes());
180 candidate.setChildren(element.getChildren());
181 return candidate;
182 }
183
184 // https://tools.ietf.org/html/draft-ietf-mmusic-ice-sip-sdp-39#section-5.1
185 public static Candidate fromSdpAttribute(final String attribute, String currentUfrag) {
186 final String[] pair = attribute.split(":", 2);
187 if (pair.length == 2 && "candidate".equals(pair[0])) {
188 final String[] segments = pair[1].split(" ");
189 if (segments.length >= 6) {
190 final String id = UUID.randomUUID().toString();
191 final String foundation = segments[0];
192 final String component = segments[1];
193 final String transport = segments[2].toLowerCase(Locale.ROOT);
194 final String priority = segments[3];
195 final String connectionAddress = segments[4];
196 final String port = segments[5];
197 final HashMap<String, String> additional = new HashMap<>();
198 for (int i = 6; i < segments.length - 1; i = i + 2) {
199 additional.put(segments[i], segments[i + 1]);
200 }
201 final String ufrag = additional.get("ufrag");
202 if (ufrag != null && !ufrag.equals(currentUfrag)) {
203 return null;
204 }
205 final Candidate candidate = new Candidate();
206 candidate.setAttribute("component", component);
207 candidate.setAttribute("foundation", foundation);
208 candidate.setAttribute("generation", additional.get("generation"));
209 candidate.setAttribute("rel-addr", additional.get("raddr"));
210 candidate.setAttribute("rel-port", additional.get("rport"));
211 candidate.setAttribute("id", id);
212 candidate.setAttribute("ip", connectionAddress);
213 candidate.setAttribute("port", port);
214 candidate.setAttribute("priority", priority);
215 candidate.setAttribute("protocol", transport);
216 candidate.setAttribute("type", additional.get("typ"));
217 return candidate;
218 }
219 }
220 return null;
221 }
222
223 public int getComponent() {
224 return getAttributeAsInt("component");
225 }
226
227 public int getFoundation() {
228 return getAttributeAsInt("foundation");
229 }
230
231 public int getGeneration() {
232 return getAttributeAsInt("generation");
233 }
234
235 public String getId() {
236 return getAttribute("id");
237 }
238
239 public String getIp() {
240 return getAttribute("ip");
241 }
242
243 public int getNetwork() {
244 return getAttributeAsInt("network");
245 }
246
247 public int getPort() {
248 return getAttributeAsInt("port");
249 }
250
251 public int getPriority() {
252 return getAttributeAsInt("priority");
253 }
254
255 public String getProtocol() {
256 return getAttribute("protocol");
257 }
258
259 public String getRelAddr() {
260 return getAttribute("rel-addr");
261 }
262
263 public int getRelPort() {
264 return getAttributeAsInt("rel-port");
265 }
266
267 public String getType() { // TODO might be converted to enum
268 return getAttribute("type");
269 }
270
271 private int getAttributeAsInt(final String name) {
272 final String value = this.getAttribute(name);
273 if (value == null) {
274 return 0;
275 }
276 try {
277 return Integer.parseInt(value);
278 } catch (NumberFormatException e) {
279 return 0;
280 }
281 }
282
283 public String toSdpAttribute(final String ufrag) {
284 final String foundation = this.getAttribute("foundation");
285 checkNotNullNoWhitespace(foundation, "foundation");
286 final String component = this.getAttribute("component");
287 checkNotNullNoWhitespace(component, "component");
288 final String protocol = this.getAttribute("protocol");
289 checkNotNullNoWhitespace(protocol, "protocol");
290 final String transport = protocol.toLowerCase(Locale.ROOT);
291 if (!"udp".equals(transport)) {
292 throw new IllegalArgumentException(
293 String.format("'%s' is not a supported protocol", transport));
294 }
295 final String priority = this.getAttribute("priority");
296 checkNotNullNoWhitespace(priority, "priority");
297 final String connectionAddress = this.getAttribute("ip");
298 checkNotNullNoWhitespace(connectionAddress, "ip");
299 final String port = this.getAttribute("port");
300 checkNotNullNoWhitespace(port, "port");
301 final Map<String, String> additionalParameter = new LinkedHashMap<>();
302 final String relAddr = this.getAttribute("rel-addr");
303 final String type = this.getAttribute("type");
304 if (type != null) {
305 additionalParameter.put("typ", type);
306 }
307 if (relAddr != null) {
308 additionalParameter.put("raddr", relAddr);
309 }
310 final String relPort = this.getAttribute("rel-port");
311 if (relPort != null) {
312 additionalParameter.put("rport", relPort);
313 }
314 final String generation = this.getAttribute("generation");
315 if (generation != null) {
316 additionalParameter.put("generation", generation);
317 }
318 if (ufrag != null) {
319 additionalParameter.put("ufrag", ufrag);
320 }
321 final String parametersString =
322 Joiner.on(' ')
323 .join(
324 Collections2.transform(
325 additionalParameter.entrySet(),
326 input ->
327 String.format(
328 "%s %s",
329 input.getKey(), input.getValue())));
330 return String.format(
331 "candidate:%s %s %s %s %s %s %s",
332 foundation,
333 component,
334 transport,
335 priority,
336 connectionAddress,
337 port,
338 parametersString);
339 }
340 }
341
342 private static void checkNotNullNoWhitespace(final String value, final String name) {
343 if (Strings.isNullOrEmpty(value)) {
344 throw new IllegalArgumentException(
345 String.format("Parameter %s is missing or empty", name));
346 }
347 SessionDescription.checkNoWhitespace(
348 value, String.format("Parameter %s contains white spaces", name));
349 }
350
351 public static class Fingerprint extends Element {
352
353 private Fingerprint() {
354 super("fingerprint", Namespace.JINGLE_APPS_DTLS);
355 }
356
357 public static Fingerprint upgrade(final Element element) {
358 Preconditions.checkArgument("fingerprint".equals(element.getName()));
359 Preconditions.checkArgument(Namespace.JINGLE_APPS_DTLS.equals(element.getNamespace()));
360 final Fingerprint fingerprint = new Fingerprint();
361 fingerprint.setAttributes(element.getAttributes());
362 fingerprint.setContent(element.getContent());
363 return fingerprint;
364 }
365
366 private static Fingerprint of(ArrayListMultimap<String, String> attributes) {
367 final String fingerprint = Iterables.getFirst(attributes.get("fingerprint"), null);
368 final String setup = Iterables.getFirst(attributes.get("setup"), null);
369 if (setup != null && fingerprint != null) {
370 final String[] fingerprintParts = fingerprint.split(" ", 2);
371 if (fingerprintParts.length == 2) {
372 final String hash = fingerprintParts[0];
373 final String actualFingerprint = fingerprintParts[1];
374 final Fingerprint element = new Fingerprint();
375 element.setAttribute("hash", hash);
376 element.setAttribute("setup", setup);
377 element.setContent(actualFingerprint);
378 return element;
379 }
380 }
381 return null;
382 }
383
384 public static Fingerprint of(
385 final SessionDescription sessionDescription, final SessionDescription.Media media) {
386 final Fingerprint fingerprint = of(media.attributes);
387 return fingerprint == null ? of(sessionDescription.attributes) : fingerprint;
388 }
389
390 private static Fingerprint of(final Setup setup, final String hash, final String content) {
391 final Fingerprint fingerprint = new Fingerprint();
392 fingerprint.setContent(content);
393 fingerprint.setAttribute("hash", hash);
394 fingerprint.setAttribute("setup", setup.toString().toLowerCase(Locale.ROOT));
395 return fingerprint;
396 }
397
398 public String getHash() {
399 return this.getAttribute("hash");
400 }
401
402 public Setup getSetup() {
403 final String setup = this.getAttribute("setup");
404 return setup == null ? null : Setup.of(setup);
405 }
406 }
407
408 public enum Setup {
409 ACTPASS,
410 PASSIVE,
411 ACTIVE;
412
413 public static Setup of(String setup) {
414 try {
415 return valueOf(setup.toUpperCase(Locale.ROOT));
416 } catch (IllegalArgumentException e) {
417 return null;
418 }
419 }
420
421 public Setup flip() {
422 if (this == PASSIVE) {
423 return ACTIVE;
424 }
425 if (this == ACTIVE) {
426 return PASSIVE;
427 }
428 throw new IllegalStateException(this.name() + " can not be flipped");
429 }
430 }
431
432 public static class IceOption extends Element {
433
434 public static final List<String> WELL_KNOWN = Arrays.asList("trickle", "renomination");
435
436 public IceOption(final String name) {
437 super(name, Namespace.JINGLE_TRANSPORT_ICE_OPTION);
438 }
439
440 public static Collection<String> of(SessionDescription.Media media) {
441 final String iceOptions = Iterables.getFirst(media.attributes.get("ice-options"), null);
442 if (Strings.isNullOrEmpty(iceOptions)) {
443 return Collections.emptyList();
444 }
445 final ImmutableList.Builder<String> optionBuilder = new ImmutableList.Builder<>();
446 for (final String iceOption : Splitter.on(' ').split(iceOptions)) {
447 if (WELL_KNOWN.contains(iceOption)) {
448 optionBuilder.add(iceOption);
449 } else {
450 Log.w(Config.LOGTAG, "unrecognized ice option: " + iceOption);
451 }
452 }
453 return optionBuilder.build();
454 }
455 }
456}