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