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