Services.java

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