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