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