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}