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