discover stun server

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/xml/Namespace.java                   |   1 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java             |   4 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java | 140 
src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java       |   6 
4 files changed, 103 insertions(+), 48 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/xml/Namespace.java 🔗

@@ -3,6 +3,7 @@ package eu.siacs.conversations.xml;
 public final class Namespace {
     public static final String DISCO_ITEMS = "http://jabber.org/protocol/disco#items";
     public static final String DISCO_INFO = "http://jabber.org/protocol/disco#info";
+    public static final String EXTERNAL_SERVICE_DISCOVERY = "urn:xmpp:extdisco:2";
     public static final String BLOCKING = "urn:xmpp:blocking";
     public static final String ROSTER = "jabber:iq:roster";
     public static final String REGISTER = "jabber:iq:register";

src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java 🔗

@@ -1902,5 +1902,9 @@ public class XmppConnection implements Runnable {
         public boolean bookmarks2() {
             return Config.USE_BOOKMARKS2 /* || hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT)*/;
         }
+
+        public boolean extendedServiceDiscovery() {
+            return hasDiscoFeature(Jid.of(account.getServer()),Namespace.EXTERNAL_SERVICE_DISCOVERY);
+        }
     }
 }

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java 🔗

@@ -16,12 +16,15 @@ import java.util.List;
 import java.util.Map;
 
 import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
 import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
 import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 import rocks.xmpp.addr.Jid;
 
@@ -51,6 +54,21 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
         super(jingleConnectionManager, id, initiator);
     }
 
+    private static State reasonToState(Reason reason) {
+        switch (reason) {
+            case SUCCESS:
+                return State.TERMINATED_SUCCESS;
+            case DECLINE:
+            case BUSY:
+                return State.TERMINATED_DECLINED_OR_BUSY;
+            case CANCEL:
+            case TIMEOUT:
+                return State.TERMINATED_CANCEL_OR_TIMEOUT;
+            default:
+                return State.TERMINATED_CONNECTIVITY_ERROR;
+        }
+    }
+
     @Override
     void deliverPacket(final JinglePacket jinglePacket) {
         Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": packet delivered to JingleRtpConnection");
@@ -85,21 +103,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
         jingleConnectionManager.finishConnection(this);
     }
 
-    private static State reasonToState(Reason reason) {
-        switch (reason) {
-            case SUCCESS:
-                return State.TERMINATED_SUCCESS;
-            case DECLINE:
-            case BUSY:
-                return State.TERMINATED_DECLINED_OR_BUSY;
-            case CANCEL:
-            case TIMEOUT:
-                return State.TERMINATED_CANCEL_OR_TIMEOUT;
-            default:
-                return State.TERMINATED_CONNECTIVITY_ERROR;
-        }
-    }
-
     private void receiveTransportInfo(final JinglePacket jinglePacket) {
         if (isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) {
             final RtpContentMap contentMap;
@@ -211,22 +214,24 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
         if (rtpContentMap == null) {
             throw new IllegalStateException("initiator RTP Content Map has not been set");
         }
-        setupWebRTC();
-        final org.webrtc.SessionDescription offer = new org.webrtc.SessionDescription(
-                org.webrtc.SessionDescription.Type.OFFER,
-                SessionDescription.of(rtpContentMap).toString()
-        );
-        try {
-            this.webRTCWrapper.setRemoteDescription(offer).get();
-            org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createAnswer().get();
-            final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description);
-            final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription);
-            sendSessionAccept(respondingRtpContentMap);
-            this.webRTCWrapper.setLocalDescription(webRTCSessionDescription);
-        } catch (Exception e) {
-            Log.d(Config.LOGTAG, "unable to send session accept", e);
+        discoverIceServers(iceServers -> {
+            setupWebRTC(iceServers);
+            final org.webrtc.SessionDescription offer = new org.webrtc.SessionDescription(
+                    org.webrtc.SessionDescription.Type.OFFER,
+                    SessionDescription.of(rtpContentMap).toString()
+            );
+            try {
+                this.webRTCWrapper.setRemoteDescription(offer).get();
+                org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createAnswer().get();
+                final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description);
+                final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription);
+                sendSessionAccept(respondingRtpContentMap);
+                this.webRTCWrapper.setLocalDescription(webRTCSessionDescription);
+            } catch (Exception e) {
+                Log.d(Config.LOGTAG, "unable to send session accept", e);
 
-        }
+            }
+        });
     }
 
     private void sendSessionAccept(final RtpContentMap rtpContentMap) {
@@ -346,17 +351,19 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
 
     private void sendSessionInitiate(final State targetState) {
         Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": prepare session-initiate");
-        setupWebRTC();
-        try {
-            org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createOffer().get();
-            final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description);
-            Log.d(Config.LOGTAG, "description: " + webRTCSessionDescription.description);
-            final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription);
-            sendSessionInitiate(rtpContentMap, targetState);
-            this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get();
-        } catch (Exception e) {
-            Log.d(Config.LOGTAG, "unable to sendSessionInitiate", e);
-        }
+        discoverIceServers(iceServers -> {
+            setupWebRTC(iceServers);
+            try {
+                org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createOffer().get();
+                final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description);
+                Log.d(Config.LOGTAG, "description: " + webRTCSessionDescription.description);
+                final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription);
+                sendSessionInitiate(rtpContentMap, targetState);
+                this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get();
+            } catch (Exception e) {
+                Log.d(Config.LOGTAG, "unable to sendSessionInitiate", e);
+            }
+        });
     }
 
     private void sendSessionInitiate(RtpContentMap rtpContentMap, final State targetState) {
@@ -481,9 +488,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
         }
     }
 
-    private void setupWebRTC() {
+    private void setupWebRTC(final List<PeerConnection.IceServer> iceServers) {
         this.webRTCWrapper.setup(this.xmppConnectionService);
-        this.webRTCWrapper.initializePeerConnection();
+        this.webRTCWrapper.initializePeerConnection(iceServers);
     }
 
     private void acceptCallFromProposed() {
@@ -559,4 +566,51 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
     private void updateEndUserState() {
         xmppConnectionService.notifyJingleRtpConnectionUpdate(id.account, id.with, id.sessionId, getEndUserState());
     }
+
+    private void discoverIceServers(final OnIceServersDiscovered onIceServersDiscovered) {
+        if (id.account.getXmppConnection().getFeatures().extendedServiceDiscovery()) {
+            final IqPacket request = new IqPacket(IqPacket.TYPE.GET);
+            request.setTo(Jid.of(id.account.getJid().getDomain()));
+            request.addChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY);
+            xmppConnectionService.sendIqPacket(id.account, request, (account, response) -> {
+                ImmutableList.Builder<PeerConnection.IceServer> listBuilder = new ImmutableList.Builder<>();
+                if (response.getType() == IqPacket.TYPE.RESULT) {
+                    final Element services = response.findChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY);
+                    final List<Element> children = services == null ? Collections.emptyList() : services.getChildren();
+                    for (final Element child : children) {
+                        if ("service".equals(child.getName())) {
+                            final String type = child.getAttribute("type");
+                            final String host = child.getAttribute("host");
+                            final String port = child.getAttribute("port");
+                            final String transport = child.getAttribute("transport");
+                            final String username = child.getAttribute("username");
+                            final String password = child.getAttribute("password");
+                            if (Arrays.asList("stun", "type").contains(type) && host != null && port != null && "udp".equals(transport)) {
+                                PeerConnection.IceServer.Builder iceServerBuilder = PeerConnection.IceServer.builder(String.format("%s:%s:%s", type, host, port));
+                                if (username != null && password != null) {
+                                    iceServerBuilder.setUsername(username);
+                                    iceServerBuilder.setPassword(password);
+                                }
+                                final PeerConnection.IceServer iceServer = iceServerBuilder.createIceServer();
+                                Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": discovered ICE Server: " + iceServer);
+                                listBuilder.add(iceServer);
+                            }
+                        }
+                    }
+                }
+                List<PeerConnection.IceServer> iceServers = listBuilder.build();
+                if (iceServers.size() == 0) {
+                    Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": no ICE server found " + response);
+                }
+                onIceServersDiscovered.onIceServersDiscovered(iceServers);
+            });
+        } else {
+            Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": has no external service discovery");
+            onIceServersDiscovered.onIceServersDiscovered(Collections.emptyList());
+        }
+    }
+
+    private interface OnIceServersDiscovered {
+        void onIceServersDiscovered(List<PeerConnection.IceServer> iceServers);
+    }
 }

src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java 🔗

@@ -132,7 +132,7 @@ public class WebRTCWrapper {
         );
     }
 
-    public void initializePeerConnection() {
+    public void initializePeerConnection(final List<PeerConnection.IceServer> iceServers) {
         PeerConnectionFactory peerConnectionFactory = PeerConnectionFactory.builder().createPeerConnectionFactory();
 
         CameraVideoCapturer capturer = null;
@@ -193,10 +193,6 @@ public class WebRTCWrapper {
 
         this.localVideoTrack = videoTrack;
 
-
-        final List<PeerConnection.IceServer> iceServers = ImmutableList.of(
-                PeerConnection.IceServer.builder("stun:xmpp.conversations.im:3478").createIceServer()
-        );
         final PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(iceServers, peerConnectionObserver);
         if (peerConnection == null) {
             throw new IllegalStateException("Unable to create PeerConnection");