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