Mercurial > libervia-backend
changeset 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 | 04a13d9ae265 |
children | 55967cf161b1 |
files | src/plugins/plugin_misc_ip.py |
diffstat | 1 files changed, 169 insertions(+), 22 deletions(-) [+] |
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)