IceUdpTransportInfo.java

  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}