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;
 18import com.google.common.collect.Multimap;
 19
 20import eu.siacs.conversations.Config;
 21import eu.siacs.conversations.xml.Element;
 22import eu.siacs.conversations.xml.Namespace;
 23import eu.siacs.conversations.xmpp.jingle.SessionDescription;
 24import eu.siacs.conversations.xmpp.jingle.transports.Transport;
 25
 26import java.util.Arrays;
 27import java.util.Collection;
 28import java.util.Collections;
 29import java.util.HashMap;
 30import java.util.Hashtable;
 31import java.util.LinkedHashMap;
 32import java.util.List;
 33import java.util.Locale;
 34import java.util.Map;
 35import java.util.UUID;
 36
 37public class IceUdpTransportInfo extends GenericTransportInfo {
 38
 39    public static final IceUdpTransportInfo STUB = new IceUdpTransportInfo();
 40
 41    public IceUdpTransportInfo() {
 42        super("transport", Namespace.JINGLE_TRANSPORT_ICE_UDP);
 43    }
 44
 45    public static IceUdpTransportInfo upgrade(final Element element) {
 46        Preconditions.checkArgument(
 47                "transport".equals(element.getName()), "Name of provided element is not transport");
 48        Preconditions.checkArgument(
 49                Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(element.getNamespace()),
 50                "Element does not match ice-udp transport namespace");
 51        final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo();
 52        transportInfo.setAttributes(element.getAttributes());
 53        transportInfo.setChildren(element.getChildren());
 54        return transportInfo;
 55    }
 56
 57    public static IceUdpTransportInfo of(
 58            SessionDescription sessionDescription, SessionDescription.Media media) {
 59        final String ufrag = Iterables.getFirst(media.attributes.get("ice-ufrag"), null);
 60        final String pwd = Iterables.getFirst(media.attributes.get("ice-pwd"), null);
 61        final IceUdpTransportInfo iceUdpTransportInfo = new IceUdpTransportInfo();
 62        if (ufrag != null) {
 63            iceUdpTransportInfo.setAttribute("ufrag", ufrag);
 64        }
 65        if (pwd != null) {
 66            iceUdpTransportInfo.setAttribute("pwd", pwd);
 67        }
 68        final Fingerprint fingerprint = Fingerprint.of(sessionDescription, media);
 69        if (fingerprint != null) {
 70            iceUdpTransportInfo.addChild(fingerprint);
 71        }
 72        for (final String iceOption : IceOption.of(media)) {
 73            iceUdpTransportInfo.addChild(new IceOption(iceOption));
 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 : this.children) {
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                && this.children.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(final String value, final String currentUfrag) {
224            final String[] segments = value.split(" ");
225            if (segments.length < 6) {
226                return null;
227            }
228            final String id = UUID.randomUUID().toString();
229            final String foundation = segments[0];
230            final String component = segments[1];
231            final String transport = segments[2].toLowerCase(Locale.ROOT);
232            final String priority = segments[3];
233            final String connectionAddress = segments[4];
234            final String port = segments[5];
235            final HashMap<String, String> additional = new HashMap<>();
236            for (int i = 6; i < segments.length - 1; i = i + 2) {
237                additional.put(segments[i], segments[i + 1]);
238            }
239            final String ufrag = additional.get("ufrag");
240            if (currentUfrag != null && ufrag != null && !ufrag.equals(currentUfrag)) {
241                return null;
242            }
243            final Candidate candidate = new Candidate();
244            candidate.setAttribute("component", component);
245            candidate.setAttribute("foundation", foundation);
246            candidate.setAttribute("generation", additional.get("generation"));
247            candidate.setAttribute("rel-addr", additional.get("raddr"));
248            candidate.setAttribute("rel-port", additional.get("rport"));
249            candidate.setAttribute("id", id);
250            candidate.setAttribute("ip", connectionAddress);
251            candidate.setAttribute("port", port);
252            candidate.setAttribute("priority", priority);
253            candidate.setAttribute("protocol", transport);
254            candidate.setAttribute("type", additional.get("typ"));
255            return candidate;
256        }
257
258        public int getComponent() {
259            return getAttributeAsInt("component");
260        }
261
262        public int getFoundation() {
263            return getAttributeAsInt("foundation");
264        }
265
266        public int getGeneration() {
267            return getAttributeAsInt("generation");
268        }
269
270        public String getId() {
271            return getAttribute("id");
272        }
273
274        public String getIp() {
275            return getAttribute("ip");
276        }
277
278        public int getNetwork() {
279            return getAttributeAsInt("network");
280        }
281
282        public int getPort() {
283            return getAttributeAsInt("port");
284        }
285
286        public int getPriority() {
287            return getAttributeAsInt("priority");
288        }
289
290        public String getProtocol() {
291            return getAttribute("protocol");
292        }
293
294        public String getRelAddr() {
295            return getAttribute("rel-addr");
296        }
297
298        public int getRelPort() {
299            return getAttributeAsInt("rel-port");
300        }
301
302        public String getType() { // TODO might be converted to enum
303            return getAttribute("type");
304        }
305
306        private int getAttributeAsInt(final String name) {
307            final String value = this.getAttribute(name);
308            if (value == null) {
309                return 0;
310            }
311            try {
312                return Integer.parseInt(value);
313            } catch (NumberFormatException e) {
314                return 0;
315            }
316        }
317
318        public String toSdpAttribute(final String ufrag) {
319            final String foundation = this.getAttribute("foundation");
320            checkNotNullNoWhitespace(foundation, "foundation");
321            final String component = this.getAttribute("component");
322            checkNotNullNoWhitespace(component, "component");
323            final String protocol = this.getAttribute("protocol");
324            checkNotNullNoWhitespace(protocol, "protocol");
325            final String transport = protocol.toLowerCase(Locale.ROOT);
326            if (!"udp".equals(transport)) {
327                throw new IllegalArgumentException(
328                        String.format("'%s' is not a supported protocol", transport));
329            }
330            final String priority = this.getAttribute("priority");
331            checkNotNullNoWhitespace(priority, "priority");
332            final String connectionAddress = this.getAttribute("ip");
333            checkNotNullNoWhitespace(connectionAddress, "ip");
334            final String port = this.getAttribute("port");
335            checkNotNullNoWhitespace(port, "port");
336            final Map<String, String> additionalParameter = new LinkedHashMap<>();
337            final String relAddr = this.getAttribute("rel-addr");
338            final String type = this.getAttribute("type");
339            if (type != null) {
340                additionalParameter.put("typ", type);
341            }
342            if (relAddr != null) {
343                additionalParameter.put("raddr", relAddr);
344            }
345            final String relPort = this.getAttribute("rel-port");
346            if (relPort != null) {
347                additionalParameter.put("rport", relPort);
348            }
349            final String generation = this.getAttribute("generation");
350            if (generation != null) {
351                additionalParameter.put("generation", generation);
352            }
353            if (ufrag != null) {
354                additionalParameter.put("ufrag", ufrag);
355            }
356            final String parametersString =
357                    Joiner.on(' ')
358                            .join(
359                                    Collections2.transform(
360                                            additionalParameter.entrySet(),
361                                            input ->
362                                                    String.format(
363                                                            "%s %s",
364                                                            input.getKey(), input.getValue())));
365            return String.format(
366                    "candidate:%s %s %s %s %s %s %s",
367                    foundation,
368                    component,
369                    transport,
370                    priority,
371                    connectionAddress,
372                    port,
373                    parametersString);
374        }
375    }
376
377    private static void checkNotNullNoWhitespace(final String value, final String name) {
378        if (Strings.isNullOrEmpty(value)) {
379            throw new IllegalArgumentException(
380                    String.format("Parameter %s is missing or empty", name));
381        }
382        SessionDescription.checkNoWhitespace(
383                value, String.format("Parameter %s contains white spaces", name));
384    }
385
386    public static class Fingerprint extends Element {
387
388        private Fingerprint() {
389            super("fingerprint", Namespace.JINGLE_APPS_DTLS);
390        }
391
392        public static Fingerprint upgrade(final Element element) {
393            Preconditions.checkArgument("fingerprint".equals(element.getName()));
394            Preconditions.checkArgument(Namespace.JINGLE_APPS_DTLS.equals(element.getNamespace()));
395            final Fingerprint fingerprint = new Fingerprint();
396            fingerprint.setAttributes(element.getAttributes());
397            fingerprint.setContent(element.getContent());
398            return fingerprint;
399        }
400
401        private static Fingerprint of(final Multimap<String, String> attributes) {
402            final String fingerprint = Iterables.getFirst(attributes.get("fingerprint"), null);
403            final String setup = Iterables.getFirst(attributes.get("setup"), null);
404            if (setup != null && fingerprint != null) {
405                final String[] fingerprintParts = fingerprint.split(" ", 2);
406                if (fingerprintParts.length == 2) {
407                    final String hash = fingerprintParts[0];
408                    final String actualFingerprint = fingerprintParts[1];
409                    final Fingerprint element = new Fingerprint();
410                    element.setAttribute("hash", hash);
411                    element.setAttribute("setup", setup);
412                    element.setContent(actualFingerprint);
413                    return element;
414                }
415            }
416            return null;
417        }
418
419        public static Fingerprint of(
420                final SessionDescription sessionDescription, final SessionDescription.Media media) {
421            final Fingerprint fingerprint = of(media.attributes);
422            return fingerprint == null ? of(sessionDescription.attributes) : fingerprint;
423        }
424
425        private static Fingerprint of(final Setup setup, final String hash, final String content) {
426            final Fingerprint fingerprint = new Fingerprint();
427            fingerprint.setContent(content);
428            fingerprint.setAttribute("hash", hash);
429            fingerprint.setAttribute("setup", setup.toString().toLowerCase(Locale.ROOT));
430            return fingerprint;
431        }
432
433        public String getHash() {
434            return this.getAttribute("hash");
435        }
436
437        public Setup getSetup() {
438            final String setup = this.getAttribute("setup");
439            return setup == null ? null : Setup.of(setup);
440        }
441    }
442
443    public enum Setup {
444        ACTPASS,
445        PASSIVE,
446        ACTIVE;
447
448        public static Setup of(String setup) {
449            try {
450                return valueOf(setup.toUpperCase(Locale.ROOT));
451            } catch (IllegalArgumentException e) {
452                return null;
453            }
454        }
455
456        public Setup flip() {
457            if (this == PASSIVE) {
458                return ACTIVE;
459            }
460            if (this == ACTIVE) {
461                return PASSIVE;
462            }
463            throw new IllegalStateException(this.name() + " can not be flipped");
464        }
465    }
466
467    public static class IceOption extends Element {
468
469        public static final List<String> WELL_KNOWN = Arrays.asList("trickle", "renomination");
470
471        public IceOption(final String name) {
472            super(name, Namespace.JINGLE_TRANSPORT_ICE_OPTION);
473        }
474
475        public static Collection<String> of(SessionDescription.Media media) {
476            final String iceOptions = Iterables.getFirst(media.attributes.get("ice-options"), null);
477            if (Strings.isNullOrEmpty(iceOptions)) {
478                return Collections.emptyList();
479            }
480            final ImmutableList.Builder<String> optionBuilder = new ImmutableList.Builder<>();
481            for (final String iceOption : Splitter.on(' ').split(iceOptions)) {
482                if (WELL_KNOWN.contains(iceOption)) {
483                    optionBuilder.add(iceOption);
484                } else {
485                    Log.w(Config.LOGTAG, "unrecognized ice option: " + iceOption);
486                }
487            }
488            return optionBuilder.build();
489        }
490    }
491}