IceUdpTransportInfo.java

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