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