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.ImmutableCollection;
 16import com.google.common.collect.ImmutableList;
 17import com.google.common.collect.Iterables;
 18
 19import eu.siacs.conversations.Config;
 20import eu.siacs.conversations.xml.Element;
 21import eu.siacs.conversations.xml.Namespace;
 22import eu.siacs.conversations.xmpp.jingle.SessionDescription;
 23
 24import java.util.Arrays;
 25import java.util.Collection;
 26import java.util.Collections;
 27import java.util.HashMap;
 28import java.util.Hashtable;
 29import java.util.LinkedHashMap;
 30import java.util.List;
 31import java.util.Locale;
 32import java.util.Map;
 33import java.util.UUID;
 34
 35public class IceUdpTransportInfo extends GenericTransportInfo {
 36
 37    public static final IceUdpTransportInfo STUB = new IceUdpTransportInfo();
 38
 39    public IceUdpTransportInfo() {
 40        super("transport", Namespace.JINGLE_TRANSPORT_ICE_UDP);
 41    }
 42
 43    public static IceUdpTransportInfo upgrade(final Element element) {
 44        Preconditions.checkArgument(
 45                "transport".equals(element.getName()), "Name of provided element is not transport");
 46        Preconditions.checkArgument(
 47                Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(element.getNamespace()),
 48                "Element does not match ice-udp transport namespace");
 49        final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo();
 50        transportInfo.setAttributes(element.getAttributes());
 51        transportInfo.setChildren(element.getChildren());
 52        return transportInfo;
 53    }
 54
 55    public static IceUdpTransportInfo of(
 56            SessionDescription sessionDescription, SessionDescription.Media media) {
 57        final String ufrag = Iterables.getFirst(media.attributes.get("ice-ufrag"), null);
 58        final String pwd = Iterables.getFirst(media.attributes.get("ice-pwd"), null);
 59        final IceUdpTransportInfo iceUdpTransportInfo = new IceUdpTransportInfo();
 60        if (ufrag != null) {
 61            iceUdpTransportInfo.setAttribute("ufrag", ufrag);
 62        }
 63        if (pwd != null) {
 64            iceUdpTransportInfo.setAttribute("pwd", pwd);
 65        }
 66        final Fingerprint fingerprint = Fingerprint.of(sessionDescription, media);
 67        if (fingerprint != null) {
 68            iceUdpTransportInfo.addChild(fingerprint);
 69        }
 70        for (final String iceOption : IceOption.of(media)) {
 71            iceUdpTransportInfo.addChild(new IceOption(iceOption));
 72        }
 73        for (final String candidate : media.attributes.get("candidate")) {
 74            iceUdpTransportInfo.addChild(Candidate.fromSdpAttributeValue(candidate, ufrag));
 75        }
 76        return iceUdpTransportInfo;
 77    }
 78
 79    public static IceUdpTransportInfo of(
 80            final Credentials credentials,
 81            final Collection<String> iceOptions,
 82            final Setup setup,
 83            final String hash,
 84            final String fingerprint) {
 85        final IceUdpTransportInfo iceUdpTransportInfo = new IceUdpTransportInfo();
 86        iceUdpTransportInfo.addChild(Fingerprint.of(setup, hash, fingerprint));
 87        iceUdpTransportInfo.setAttribute("ufrag", credentials.ufrag);
 88        iceUdpTransportInfo.setAttribute("pwd", credentials.password);
 89        for (final String iceOption : iceOptions) {
 90            iceUdpTransportInfo.addChild(new IceOption(iceOption));
 91        }
 92        return iceUdpTransportInfo;
 93    }
 94
 95    public Fingerprint getFingerprint() {
 96        final Element fingerprint = this.findChild("fingerprint", Namespace.JINGLE_APPS_DTLS);
 97        return fingerprint == null ? null : Fingerprint.upgrade(fingerprint);
 98    }
 99
100    public List<String> getIceOptions() {
101        final ImmutableList.Builder<String> optionBuilder = new ImmutableList.Builder<>();
102        for (final Element child : getChildren()) {
103            if (Namespace.JINGLE_TRANSPORT_ICE_OPTION.equals(child.getNamespace())
104                    && IceOption.WELL_KNOWN.contains(child.getName())) {
105                optionBuilder.add(child.getName());
106            }
107        }
108        return optionBuilder.build();
109    }
110
111    public Credentials getCredentials() {
112        final String ufrag = this.getAttribute("ufrag");
113        final String password = this.getAttribute("pwd");
114        return new Credentials(ufrag, password);
115    }
116
117    public boolean isStub() {
118        return Strings.isNullOrEmpty(this.getAttribute("ufrag"))
119                && Strings.isNullOrEmpty(this.getAttribute("pwd"))
120                && getChildren().isEmpty();
121    }
122
123    public List<Candidate> getCandidates() {
124        final ImmutableList.Builder<Candidate> builder = new ImmutableList.Builder<>();
125        for (final Element child : getChildren()) {
126            if ("candidate".equals(child.getName())) {
127                builder.add(Candidate.upgrade(child));
128            }
129        }
130        return builder.build();
131    }
132
133    public IceUdpTransportInfo cloneWrapper() {
134        final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo();
135        transportInfo.setAttributes(new Hashtable<>(getAttributes()));
136        return transportInfo;
137    }
138
139    public IceUdpTransportInfo modifyCredentials(final Credentials credentials, final Setup setup) {
140        final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo();
141        transportInfo.setAttribute("ufrag", credentials.ufrag);
142        transportInfo.setAttribute("pwd", credentials.password);
143        for (final Element child : getChildren()) {
144            if (child.getName().equals("fingerprint")
145                    && Namespace.JINGLE_APPS_DTLS.equals(child.getNamespace())) {
146                final Fingerprint fingerprint = new Fingerprint();
147                fingerprint.setAttributes(new Hashtable<>(child.getAttributes()));
148                fingerprint.setContent(child.getContent());
149                fingerprint.setAttribute("setup", setup.toString().toLowerCase(Locale.ROOT));
150                transportInfo.addChild(fingerprint);
151            }
152        }
153        for (final String iceOption : this.getIceOptions()) {
154            transportInfo.addChild(new IceOption(iceOption));
155        }
156        return transportInfo;
157    }
158
159    public IceUdpTransportInfo withCandidates(ImmutableCollection<Candidate> candidates) {
160        final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo();
161        transportInfo.setAttributes(new Hashtable<>(getAttributes()));
162        transportInfo.setChildren(this.getChildren());
163        for(final Candidate candidate : candidates) {
164            transportInfo.addChild(candidate);
165        }
166        return transportInfo;
167    }
168
169    public static class Credentials {
170        public final String ufrag;
171        public final String password;
172
173        public Credentials(String ufrag, String password) {
174            this.ufrag = ufrag;
175            this.password = password;
176        }
177
178        @Override
179        public boolean equals(Object o) {
180            if (this == o) return true;
181            if (o == null || getClass() != o.getClass()) return false;
182            Credentials that = (Credentials) o;
183            return Objects.equal(ufrag, that.ufrag) && Objects.equal(password, that.password);
184        }
185
186        @Override
187        public int hashCode() {
188            return Objects.hashCode(ufrag, password);
189        }
190
191        @Override
192        @NonNull
193        public String toString() {
194            return MoreObjects.toStringHelper(this)
195                    .add("ufrag", ufrag)
196                    .add("password", password)
197                    .toString();
198        }
199    }
200
201    public static class Candidate extends Element {
202
203        private Candidate() {
204            super("candidate");
205        }
206
207        public static Candidate upgrade(final Element element) {
208            Preconditions.checkArgument("candidate".equals(element.getName()));
209            final Candidate candidate = new Candidate();
210            candidate.setAttributes(element.getAttributes());
211            candidate.setChildren(element.getChildren());
212            return candidate;
213        }
214
215        // https://tools.ietf.org/html/draft-ietf-mmusic-ice-sip-sdp-39#section-5.1
216        public static Candidate fromSdpAttribute(final String attribute, String currentUfrag) {
217            final String[] pair = attribute.split(":", 2);
218            if (pair.length == 2 && "candidate".equals(pair[0])) {
219                return fromSdpAttributeValue(pair[1], currentUfrag);
220            }
221            return null;
222        }
223
224        public static Candidate fromSdpAttributeValue(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(ArrayListMultimap<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}