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
This commit is contained in:
Goffi 2015-09-29 17:54:25 +02:00
commit c23c6c39b8

View file

@ -22,14 +22,24 @@ from sat.core.constants import Const as C
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 = """
</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):
client = self.host.getClient(profile)
# XXX: we keep cache only for profile session as ip can change between them
client._ip_cache = None
# NAT-Port
try:
self._nat = host.plugins['NAT-PORT']
except KeyError:
log.debug(u"NAT port plugin not available")
self._nat = None
def getIP(self, profile):
# 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)
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)
# 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
if self._ip_cache is not None:
defer.returnValue(self._ip_cache)
allow_get_ip = self.host.memory.params.getParamA(GET_IP_NAME, GET_IP_CATEGORY, use_default=False)
# 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)
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)
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)