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