IceUdpTransportInfo.java

  1package eu.siacs.conversations.xmpp.jingle.stanzas;
  2
  3import android.util.Log;
  4import androidx.annotation.NonNull;
  5import com.google.common.base.Joiner;
  6import com.google.common.base.MoreObjects;
  7import com.google.common.base.Objects;
  8import com.google.common.base.Preconditions;
  9import com.google.common.base.Splitter;
 10import com.google.common.base.Strings;
 11import com.google.common.collect.Collections2;
 12import com.google.common.collect.ImmutableCollection;
 13import com.google.common.collect.ImmutableList;
 14import com.google.common.collect.Iterables;
 15import com.google.common.collect.Multimap;
 16import eu.siacs.conversations.Config;
 17import eu.siacs.conversations.xml.Element;
 18import eu.siacs.conversations.xml.Namespace;
 19import eu.siacs.conversations.xmpp.jingle.SessionDescription;
 20import eu.siacs.conversations.xmpp.jingle.transports.Transport;
 21import java.util.Arrays;
 22import java.util.Collection;
 23import java.util.Collections;
 24import java.util.HashMap;
 25import java.util.Hashtable;
 26import java.util.LinkedHashMap;
 27import java.util.List;
 28import java.util.Locale;
 29import java.util.Map;
 30import java.util.UUID;
 31
 32public class IceUdpTransportInfo extends GenericTransportInfo {
 33
 34    public static final IceUdpTransportInfo STUB = new IceUdpTransportInfo();
 35
 36    public IceUdpTransportInfo() {
 37        super("transport", Namespace.JINGLE_TRANSPORT_ICE_UDP);
 38    }
 39
 40    public static IceUdpTransportInfo upgrade(final Element element) {
 41        Preconditions.checkArgument(
 42                "transport".equals(element.getName()), "Name of provided element is not transport");
 43        Preconditions.checkArgument(
 44                Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(element.getNamespace()),
 45                "Element does not match ice-udp transport namespace");
 46        final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo();
 47        transportInfo.setAttributes(element.getAttributes());
 48        transportInfo.setChildren(element.getChildren());
 49        return transportInfo;
 50    }
 51
 52    public static IceUdpTransportInfo of(
 53            SessionDescription sessionDescription, SessionDescription.Media media) {
 54        final String ufrag = Iterables.getFirst(media.attributes.get("ice-ufrag"), null);
 55        final String pwd = Iterables.getFirst(media.attributes.get("ice-pwd"), null);
 56        final IceUdpTransportInfo iceUdpTransportInfo = new IceUdpTransportInfo();
 57        if (ufrag != null) {
 58            iceUdpTransportInfo.setAttribute("ufrag", ufrag);
 59        }
 60        if (pwd != null) {
 61            iceUdpTransportInfo.setAttribute("pwd", pwd);
 62        }
 63        final Fingerprint fingerprint = Fingerprint.of(sessionDescription, media);
 64        if (fingerprint != null) {
 65            iceUdpTransportInfo.addChild(fingerprint);
 66        }
 67        for (final String iceOption : IceOption.of(media)) {
 68            iceUdpTransportInfo.addChild(new IceOption(iceOption));
 69        }
 70        return iceUdpTransportInfo;
 71    }
 72
 73    public static IceUdpTransportInfo of(
 74            final Credentials credentials,
 75            final Collection<String> iceOptions,
 76            final Setup setup,
 77            final String hash,
 78            final String fingerprint) {
 79        final IceUdpTransportInfo iceUdpTransportInfo = new IceUdpTransportInfo();
 80        iceUdpTransportInfo.addChild(Fingerprint.of(setup, hash, fingerprint));
 81        iceUdpTransportInfo.setAttribute("ufrag", credentials.ufrag);
 82        iceUdpTransportInfo.setAttribute("pwd", credentials.password);
 83        for (final String iceOption : iceOptions) {
 84            iceUdpTransportInfo.addChild(new IceOption(iceOption));
 85        }
 86        return iceUdpTransportInfo;
 87    }
 88
 89    public Fingerprint getFingerprint() {
 90        final Element fingerprint = this.findChild("fingerprint", Namespace.JINGLE_APPS_DTLS);
 91        return fingerprint == null ? null : Fingerprint.upgrade(fingerprint);
 92    }
 93
 94    public List<String> getIceOptions() {
 95        final ImmutableList.Builder<String> optionBuilder = new ImmutableList.Builder<>();
 96        for (final Element child : this.children) {
 97            if (Namespace.JINGLE_TRANSPORT_ICE_OPTION.equals(child.getNamespace())
 98                    && IceOption.WELL_KNOWN.contains(child.getName())) {
 99                optionBuilder.add(
100                        SessionDescription.checkNoWhitespace(
101                                child.getName(), "Ice options should not contain whitespace"));
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 IceUdpTransportInfo withCandidates(ImmutableCollection<Candidate> candidates) {
156        final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo();
157        transportInfo.setAttributes(new Hashtable<>(getAttributes()));
158        transportInfo.setChildren(this.getChildren());
159        for (final Candidate candidate : candidates) {
160            transportInfo.addChild(candidate);
161        }
162        return transportInfo;
163    }
164
165    public static class Credentials {
166        public final String ufrag;
167        public final String password;
168
169        public Credentials(String ufrag, String password) {
170            this.ufrag = ufrag;
171            this.password = password;
172        }
173
174        @Override
175        public boolean equals(Object o) {
176            if (this == o) return true;
177            if (o == null || getClass() != o.getClass()) return false;
178            Credentials that = (Credentials) o;
179            return Objects.equal(ufrag, that.ufrag) && Objects.equal(password, that.password);
180        }
181
182        @Override
183        public int hashCode() {
184            return Objects.hashCode(ufrag, password);
185        }
186
187        @Override
188        @NonNull
189        public String toString() {
190            return MoreObjects.toStringHelper(this)
191                    .add("ufrag", ufrag)
192                    .add("password", password)
193                    .toString();
194        }
195    }
196
197    public static class Candidate extends Element implements Transport.Candidate {
198
199        private Candidate() {
200            super("candidate");
201        }
202
203        public static Candidate upgrade(final Element element) {
204            Preconditions.checkArgument("candidate".equals(element.getName()));
205            final Candidate candidate = new Candidate();
206            candidate.setAttributes(element.getAttributes());
207            candidate.setChildren(element.getChildren());
208            return candidate;
209        }
210
211        // https://tools.ietf.org/html/draft-ietf-mmusic-ice-sip-sdp-39#section-5.1
212        public static Candidate fromSdpAttribute(final String attribute, String currentUfrag) {
213            final String[] pair = attribute.split(":", 2);
214            if (pair.length == 2 && "candidate".equals(pair[0])) {
215                return fromSdpAttributeValue(pair[1], currentUfrag);
216            }
217            return null;
218        }
219
220        public static Candidate fromSdpAttributeValue(
221                final String value, final String currentUfrag) {
222            final String[] segments = value.split(" ");
223            if (segments.length < 6) {
224                return null;
225            }
226            final String id = UUID.randomUUID().toString();
227            final String foundation = segments[0];
228            final String component = segments[1];
229            final String transport = segments[2].toLowerCase(Locale.ROOT);
230            final String priority = segments[3];
231            final String connectionAddress = segments[4];
232            final String port = segments[5];
233            final HashMap<String, String> additional = new HashMap<>();
234            for (int i = 6; i < segments.length - 1; i = i + 2) {
235                additional.put(segments[i], segments[i + 1]);
236            }
237            final String ufrag = additional.get("ufrag");
238            if (currentUfrag != null && ufrag != null && !ufrag.equals(currentUfrag)) {
239                return null;
240            }
241            final Candidate candidate = new Candidate();
242            candidate.setAttribute("component", component);
243            candidate.setAttribute("foundation", foundation);
244            candidate.setAttribute("generation", additional.get("generation"));
245            candidate.setAttribute("rel-addr", additional.get("raddr"));
246            candidate.setAttribute("rel-port", additional.get("rport"));
247            candidate.setAttribute("id", id);
248            candidate.setAttribute("ip", connectionAddress);
249            candidate.setAttribute("port", port);
250            candidate.setAttribute("priority", priority);
251            candidate.setAttribute("protocol", transport);
252            candidate.setAttribute("type", additional.get("typ"));
253            return candidate;
254        }
255
256        public int getComponent() {
257            return getAttributeAsInt("component");
258        }
259
260        public int getFoundation() {
261            return getAttributeAsInt("foundation");
262        }
263
264        public int getGeneration() {
265            return getAttributeAsInt("generation");
266        }
267
268        public String getId() {
269            return getAttribute("id");
270        }
271
272        public String getIp() {
273            return getAttribute("ip");
274        }
275
276        public int getNetwork() {
277            return getAttributeAsInt("network");
278        }
279
280        public int getPort() {
281            return getAttributeAsInt("port");
282        }
283
284        public int getPriority() {
285            return getAttributeAsInt("priority");
286        }
287
288        public String getProtocol() {
289            return getAttribute("protocol");
290        }
291
292        public String getRelAddr() {
293            return getAttribute("rel-addr");
294        }
295
296        public int getRelPort() {
297            return getAttributeAsInt("rel-port");
298        }
299
300        public String getType() { // TODO might be converted to enum
301            return getAttribute("type");
302        }
303
304        private int getAttributeAsInt(final String name) {
305            final String value = this.getAttribute(name);
306            if (value == null) {
307                return 0;
308            }
309            try {
310                return Integer.parseInt(value);
311            } catch (NumberFormatException e) {
312                return 0;
313            }
314        }
315
316        public String toSdpAttribute(final String ufrag) {
317            final String foundation = this.getAttribute("foundation");
318            checkNotNullNoWhitespace(foundation, "foundation");
319            final String component = this.getAttribute("component");
320            checkNotNullNoWhitespace(component, "component");
321            final String protocol = this.getAttribute("protocol");
322            checkNotNullNoWhitespace(protocol, "protocol");
323            final String transport = protocol.toLowerCase(Locale.ROOT);
324            if (!"udp".equals(transport)) {
325                throw new IllegalArgumentException(
326                        String.format("'%s' is not a supported protocol", transport));
327            }
328            final String priority = this.getAttribute("priority");
329            checkNotNullNoWhitespace(priority, "priority");
330            final String connectionAddress = this.getAttribute("ip");
331            checkNotNullNoWhitespace(connectionAddress, "ip");
332            final String port = this.getAttribute("port");
333            checkNotNullNoWhitespace(port, "port");
334            final Map<String, String> additionalParameter = new LinkedHashMap<>();
335            final String relAddr = this.getAttribute("rel-addr");
336            final String type = this.getAttribute("type");
337            if (type != null) {
338                additionalParameter.put("typ", type);
339            }
340            if (relAddr != null) {
341                additionalParameter.put("raddr", relAddr);
342            }
343            final String relPort = this.getAttribute("rel-port");
344            if (relPort != null) {
345                additionalParameter.put("rport", relPort);
346            }
347            final String generation = this.getAttribute("generation");
348            if (generation != null) {
349                additionalParameter.put("generation", generation);
350            }
351            if (ufrag != null) {
352                additionalParameter.put("ufrag", ufrag);
353            }
354            final String parametersString =
355                    Joiner.on(' ')
356                            .join(
357                                    Collections2.transform(
358                                            additionalParameter.entrySet(),
359                                            input ->
360                                                    String.format(
361                                                            "%s %s",
362                                                            input.getKey(), input.getValue())));
363            return String.format(
364                    "candidate:%s %s %s %s %s %s %s",
365                    foundation,
366                    component,
367                    transport,
368                    priority,
369                    connectionAddress,
370                    port,
371                    parametersString);
372        }
373    }
374
375    private static void checkNotNullNoWhitespace(final String value, final String name) {
376        if (Strings.isNullOrEmpty(value)) {
377            throw new IllegalArgumentException(
378                    String.format("Parameter %s is missing or empty", name));
379        }
380        SessionDescription.checkNoWhitespace(
381                value, String.format("Parameter %s contains white spaces", name));
382    }
383
384    public static class Fingerprint extends Element {
385
386        private Fingerprint() {
387            super("fingerprint", Namespace.JINGLE_APPS_DTLS);
388        }
389
390        public static Fingerprint upgrade(final Element element) {
391            Preconditions.checkArgument("fingerprint".equals(element.getName()));
392            Preconditions.checkArgument(Namespace.JINGLE_APPS_DTLS.equals(element.getNamespace()));
393            final Fingerprint fingerprint = new Fingerprint();
394            fingerprint.setAttributes(element.getAttributes());
395            fingerprint.setContent(element.getContent());
396            return fingerprint;
397        }
398
399        private static Fingerprint of(final Multimap<String, String> attributes) {
400            final String fingerprint = Iterables.getFirst(attributes.get("fingerprint"), null);
401            final String setup = Iterables.getFirst(attributes.get("setup"), null);
402            if (setup != null && fingerprint != null) {
403                final String[] fingerprintParts = fingerprint.split(" ", 2);
404                if (fingerprintParts.length == 2) {
405                    final String hash = fingerprintParts[0];
406                    final String actualFingerprint = fingerprintParts[1];
407                    final Fingerprint element = new Fingerprint();
408                    element.setAttribute("hash", hash);
409                    element.setAttribute("setup", setup);
410                    element.setContent(actualFingerprint);
411                    return element;
412                }
413            }
414            return null;
415        }
416
417        public static Fingerprint of(
418                final SessionDescription sessionDescription, final SessionDescription.Media media) {
419            final Fingerprint fingerprint = of(media.attributes);
420            return fingerprint == null ? of(sessionDescription.attributes) : fingerprint;
421        }
422
423        private static Fingerprint of(final Setup setup, final String hash, final String content) {
424            final Fingerprint fingerprint = new Fingerprint();
425            fingerprint.setContent(content);
426            fingerprint.setAttribute("hash", hash);
427            fingerprint.setAttribute("setup", setup.toString().toLowerCase(Locale.ROOT));
428            return fingerprint;
429        }
430
431        public String getHash() {
432            return this.getAttribute("hash");
433        }
434
435        public Setup getSetup() {
436            final String setup = this.getAttribute("setup");
437            return setup == null ? null : Setup.of(setup);
438        }
439    }
440
441    public enum Setup {
442        ACTPASS,
443        PASSIVE,
444        ACTIVE;
445
446        public static Setup of(String setup) {
447            try {
448                return valueOf(setup.toUpperCase(Locale.ROOT));
449            } catch (IllegalArgumentException e) {
450                return null;
451            }
452        }
453
454        public Setup flip() {
455            if (this == PASSIVE) {
456                return ACTIVE;
457            }
458            if (this == ACTIVE) {
459                return PASSIVE;
460            }
461            throw new IllegalStateException(this.name() + " can not be flipped");
462        }
463    }
464
465    public static class IceOption extends Element {
466
467        public static final List<String> WELL_KNOWN = Arrays.asList("trickle", "renomination");
468
469        public IceOption(final String name) {
470            super(name, Namespace.JINGLE_TRANSPORT_ICE_OPTION);
471        }
472
473        public static Collection<String> of(SessionDescription.Media media) {
474            final String iceOptions = Iterables.getFirst(media.attributes.get("ice-options"), null);
475            if (Strings.isNullOrEmpty(iceOptions)) {
476                return Collections.emptyList();
477            }
478            final ImmutableList.Builder<String> optionBuilder = new ImmutableList.Builder<>();
479            for (final String iceOption : Splitter.on(' ').split(iceOptions)) {
480                if (WELL_KNOWN.contains(iceOption)) {
481                    optionBuilder.add(iceOption);
482                } else {
483                    Log.w(Config.LOGTAG, "unrecognized ice option: " + iceOption);
484                }
485            }
486            return optionBuilder.build();
487        }
488    }
489}