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}