diff src/plugins/plugin_misc_ip.py @ 1537:6fa9e8c02c34

plugin IP discovery: better IP discovering: - locals and external IP addresses can be discovered - if netifaces is available, it is used - same thing for NAT Port plugin - cache is no more managed per profile but globally, a signal or observer will be used in the future to detect connection changes
author Goffi <goffi@goffi.org>
date Tue, 29 Sep 2015 17:54:25 +0200
parents a5e0393a06cd
children eb8aae35085b
line wrap: on
line diff
--- a/src/plugins/plugin_misc_ip.py	Tue Sep 29 17:54:24 2015 +0200
+++ b/src/plugins/plugin_misc_ip.py	Tue Sep 29 17:54:25 2015 +0200
@@ -22,14 +22,24 @@
 from sat.core.log import getLogger
 log = getLogger(__name__)
 from sat.tools import xml_tools
-from twisted.web.client import getPage
+from twisted.web import client as webclient
+from twisted.internet import defer
+from twisted.internet import reactor
+from twisted.internet import protocol
+from twisted.internet import endpoints
 import urlparse
+try:
+    import netifaces
+except ImportError:
+    log.warning(u"netifaces is not available, it help discovering IPs, you can install it on https://pypi.python.org/pypi/netifaces")
+    netifaces = None
 
 
 PLUGIN_INFO = {
     "name": "IP discovery",
     "import_name": "IP",
     "type": C.PLUG_TYPE_MISC,
+    "recommendations": ["NAT-PORT"],
     "main": "IPPlugin",
     "handler": "no",
     "description": _("""This plugin help to discover our external IP address.""")
@@ -62,40 +72,177 @@
     </params>
     """.format(category=GET_IP_CATEGORY, name=GET_IP_NAME, label=GET_IP_LABEL)
 
+
 class IPPlugin(object):
+    # TODO: refresh IP if a new connection is detected
+    # TODO: manage IPv6 when implemented in SàT
+    # TODO: implement XEP-0279
 
     def __init__(self, host):
         log.info(_("plugin IP discovery initialization"))
         self.host = host
         host.memory.updateParams(PARAMS)
 
-    def profileConnected(self, profile):
+        # NAT-Port
+        try:
+            self._nat = host.plugins['NAT-PORT']
+        except KeyError:
+            log.debug(u"NAT port plugin not available")
+            self._nat = None
+
+        # XXX: cache is kept until SàT is restarted
+        #      if IP may have changed, use self.refreshIP
+        self._ip_cache = None
+
+    def refreshIP(self):
+        # FIXME: use a trigger instead ?
+        self._ip_cache = None
+
+    def _externalAllowed(self, profile):
+        """Return value of parameter with autorisation of user to do external requests
+
+        if parameter is not set, a dialog is shown to use to get its confirmation, and parameted is set according to answer
+        @param profile: %(doc_profile)s
+        @return (defer.Deferred[bool]): True if external request is autorised
+        """
+        allow_get_ip = self.host.memory.params.getParamA(GET_IP_NAME, GET_IP_CATEGORY, use_default=False)
+
+        if allow_get_ip is None:
+            # we don't have autorisation from user yet to use get_ip, we ask him
+            def setParam(allowed):
+                # FIXME: we need to use boolConst as setParam only manage str/unicode
+                #        need to be fixed when params will be refactored
+                self.host.memory.setParam(GET_IP_NAME, C.boolConst(allow_get_ip), GET_IP_CATEGORY)
+                return allowed
+            d = xml_tools.deferConfirm(self.host, _(GET_IP_CONFIRM), _(GET_IP_CONFIRM_TITLE), profile=profile)
+            d.addCallback(setParam)
+            return d
+
+        return defer.succeed(allow_get_ip)
+
+    def _filterAddresse(self, ip_addr):
+        """Filter acceptable addresses
+
+        For now, just remove IPv4 local addresses
+        @param ip_addr(str): IP addresse
+        @return (bool): True if addresse is acceptable
+        """
+        return not ip_addr.startswith('127.')
+
+    def _insertFirst(self, addresses, ip_addr):
+        """Insert ip_addr as first item in addresses
+
+        @param ip_addr(str): IP addresse
+        @param addresses(list): list of IP addresses
+        """
+        if ip_addr in addresses:
+            if addresses[0] != ip_addr:
+                addresses.remove(ip_addr)
+                addresses.insert(0, ip_addr)
+        else:
+            addresses.insert(0, ip_addr)
+
+    def _getIPFromExternal(self, ext_url):
+        """Get local IP by doing a connection on an external url
+
+        @param ext_utl(str): url to connect to
+        @return (defer.Deferred): return local IP
+        """
+        url = urlparse.urlparse(ext_url)
+        port = url.port
+        if port is None:
+            if url.scheme=='http':
+                port = 80
+            elif url.scheme=='https':
+                port = 443
+            else:
+                log.error(u"Unknown url scheme: {}".format(url.scheme))
+                defer.returnValue(None)
+        if url.hostname is None:
+            log.error(u"Can't find url hostname for {}".format(GET_IP_PAGE))
+
+        point = endpoints.TCP4ClientEndpoint(reactor, url.hostname, port)
+        def gotConnection(p):
+            local_ip = p.transport.getHost().host
+            p.transport.loseConnection()
+            return local_ip
+
+        d = endpoints.connectProtocol(point, protocol.Protocol())
+        d.addCallback(gotConnection)
+        return d
+
+
+    @defer.inlineCallbacks
+    def getLocalIPs(self, profile):
+        """Try do discover local area network IPs
+
+        @param profile): %(doc_profile)s
+        @return (deferred): list of lan IP addresses
+            or empty list if it can't be discovered
+            if there are several addresses, the one used with the server is put first
+        """
+        # TODO: manage permission requesting (e.g. for UMTS link)
         client = self.host.getClient(profile)
-        # XXX: we keep cache only for profile session as ip can change between them
-        client._ip_cache = None
+        addresses = []
+
+        # we first try our luck with netifaces
+        if netifaces is not None:
+            addresses = []
+            for interface in netifaces.interfaces():
+                if_addresses = netifaces.ifaddresses(interface)
+                try:
+                    inet_list = if_addresses[netifaces.AF_INET]
+                except KeyError:
+                    continue
+                for data in inet_list:
+                    addresse = data['addr']
+                    if self._filterAddresse(addresse):
+                        addresses.append(addresse)
+
+        # we first try with our connection to server
+        ip = client.xmlstream.transport.getHost().host
+        if self._filterAddresse(ip):
+            self._insertFirst(addresses, ip)
+            defer.returnValue(addresses)
 
-    def getIP(self, profile):
+        # if not available, we try with NAT-Port
+        if self._nat is not None:
+            nat_ip = yield self._nat.getIP(local=True)
+            if nat_ip is not None:
+                self._insertFirst(addresses, nat_ip)
+                defer.returnValue(addresses)
+
+            if addresses:
+                defer.returnValue(addresses)
+
+            # still not luck, we need to contact external website
+            allow_get_ip = yield self._externalAllowed(profile)
+
+            if not allow_get_ip:
+                defer.returnValue(addresses)
+
+        ip_tuple = yield self._getIPFromExternal(GET_IP_PAGE)
+        self._insertFirst(addresses, ip_tuple.local)
+        defer.returnValue(addresses)
+
+
+    @defer.inlineCallbacks
+    def getExternalIP(self, profile):
         """Try to discover external IP
 
         @param profile: %(doc_profile)s
         @return (deferred): external IP address or None if it can't be discovered
         """
-        client = self.host.getClient(profile)
-        if client._ip_cache is not None:
-            return client._ip_cache
-
-        allow_get_ip = self.host.memory.params.getParamA(GET_IP_NAME, GET_IP_CATEGORY, use_default=False)
+        if self._ip_cache is not None:
+            defer.returnValue(self._ip_cache)
 
-        if allow_get_ip is None:
-            # we don't have autorisation from user yet to use get_ip, we ask him
-            confirm_d = xml_tools.deferConfirm(self.host, _(GET_IP_CONFIRM), _(GET_IP_CONFIRM_TITLE), profile=profile)
-            def setParam(allowed):
-                # FIXME: we need to use boolConst as setParam only manage str/unicode
-                #        need to be fixed when params will be refactored
-                self.host.memory.setParam(GET_IP_NAME, C.boolConst(allowed), GET_IP_CATEGORY)
-                return self.getIP(profile)
+        # we first try with NAT-Port
+        if self._nat is not None:
+            nat_ip = yield self._nat.getIP()
+            if nat_ip is not None:
+                defer.returnValue(nat_ip)
 
-            return confirm_d.addCallback(setParam)
-
-
-        return getPage(GET_IP_PAGE) if allow_get_ip else None
+        # then by requesting external website
+        allow_get_ip = yield self._externalAllowed(profile)
+        ip = webclient.getPage(GET_IP_PAGE) if allow_get_ip else None
+        defer.returnValue(ip)