Services.java

  1package im.conversations.android.xmpp.model.disco.external;
  2
  3import android.util.Log;
  4import androidx.annotation.NonNull;
  5import com.google.common.base.Objects;
  6import com.google.common.base.Strings;
  7import com.google.common.collect.Collections2;
  8import com.google.common.collect.ImmutableSet;
  9import com.google.common.primitives.Ints;
 10import eu.siacs.conversations.Config;
 11import eu.siacs.conversations.utils.IP;
 12import im.conversations.android.annotation.XmlElement;
 13import im.conversations.android.xmpp.model.Extension;
 14import java.util.Arrays;
 15import java.util.Collection;
 16import org.webrtc.PeerConnection;
 17
 18@XmlElement
 19public class Services extends Extension {
 20
 21    public Services() {
 22        super(Services.class);
 23    }
 24
 25    public Collection<Service> getServices() {
 26        return this.getExtensions(Service.class);
 27    }
 28
 29    public Collection<PeerConnection.IceServer> getIceServers() {
 30        final var builder = new ImmutableSet.Builder<IceServerWrapper>();
 31        for (final var service : this.getServices()) {
 32            final String type = service.getAttribute("type");
 33            final String host = service.getAttribute("host");
 34            final String sport = service.getAttribute("port");
 35            final Integer port = sport == null ? null : Ints.tryParse(sport);
 36            final String transport = service.getAttribute("transport");
 37            final String username = service.getAttribute("username");
 38            final String password = service.getAttribute("password");
 39            if (Strings.isNullOrEmpty(host) || port == null) {
 40                continue;
 41            }
 42            if (port < 0 || port > 65535) {
 43                continue;
 44            }
 45
 46            if (Arrays.asList("stun", "stuns", "turn", "turns").contains(type)
 47                    && Arrays.asList("udp", "tcp").contains(transport)) {
 48                if (Arrays.asList("stuns", "turns").contains(type) && "udp".equals(transport)) {
 49                    Log.w(
 50                            Config.LOGTAG,
 51                            "skipping invalid combination of udp/tls in external services");
 52                    continue;
 53                }
 54
 55                // STUN URLs do not support a query section since M110
 56                final String uri;
 57                if (Arrays.asList("stun", "stuns").contains(type)) {
 58                    uri = String.format("%s:%s:%s", type, IP.wrapIPv6(host), port);
 59                } else {
 60                    uri =
 61                            String.format(
 62                                    "%s:%s:%s?transport=%s",
 63                                    type, IP.wrapIPv6(host), port, transport);
 64                }
 65
 66                final PeerConnection.IceServer.Builder iceServerBuilder =
 67                        PeerConnection.IceServer.builder(uri);
 68                iceServerBuilder.setTlsCertPolicy(
 69                        PeerConnection.TlsCertPolicy.TLS_CERT_POLICY_INSECURE_NO_CHECK);
 70                if (username != null && password != null) {
 71                    iceServerBuilder.setUsername(username);
 72                    iceServerBuilder.setPassword(password);
 73                } else if (Arrays.asList("turn", "turns").contains(type)) {
 74                    // The WebRTC spec requires throwing an
 75                    // InvalidAccessError on empty username or password
 76                    // https://chromium.googlesource.com/external/webrtc/+/master/pc/ice_server_parsing.cc
 77                    Log.w(
 78                            Config.LOGTAG,
 79                            "skipping "
 80                                    + type
 81                                    + "/"
 82                                    + transport
 83                                    + " without username and password");
 84                    continue;
 85                }
 86                final var iceServer = new IceServerWrapper(iceServerBuilder.createIceServer());
 87                Log.w(Config.LOGTAG, "discovered ICE Server: " + iceServer);
 88                builder.add(iceServer);
 89            }
 90        }
 91        final var set = builder.build();
 92        Log.d(Config.LOGTAG, "discovered " + set.size() + " ice servers");
 93        return Collections2.transform(set, i -> i.iceServer);
 94    }
 95
 96    private static class IceServerWrapper {
 97
 98        private final PeerConnection.IceServer iceServer;
 99
100        private IceServerWrapper(final PeerConnection.IceServer iceServer) {
101            this.iceServer = iceServer;
102        }
103
104        @Override
105        public boolean equals(Object o) {
106            if (this == o) return true;
107            if (!(o instanceof IceServerWrapper that)) return false;
108            return Objects.equal(iceServer.urls, that.iceServer.urls)
109                    && Objects.equal(iceServer.username, that.iceServer.username)
110                    && Objects.equal(iceServer.password, that.iceServer.password);
111        }
112
113        @Override
114        public int hashCode() {
115            return Objects.hashCode(iceServer.urls, iceServer.urls, iceServer.password);
116        }
117
118        @Override
119        @NonNull
120        public String toString() {
121            return this.iceServer.toString();
122        }
123    }
124}