comparison 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
comparison
equal deleted inserted replaced
1536:04a13d9ae265 1537:6fa9e8c02c34
20 from sat.core.i18n import _, D_ 20 from sat.core.i18n import _, D_
21 from sat.core.constants import Const as C 21 from sat.core.constants import Const as C
22 from sat.core.log import getLogger 22 from sat.core.log import getLogger
23 log = getLogger(__name__) 23 log = getLogger(__name__)
24 from sat.tools import xml_tools 24 from sat.tools import xml_tools
25 from twisted.web.client import getPage 25 from twisted.web import client as webclient
26 from twisted.internet import defer
27 from twisted.internet import reactor
28 from twisted.internet import protocol
29 from twisted.internet import endpoints
26 import urlparse 30 import urlparse
31 try:
32 import netifaces
33 except ImportError:
34 log.warning(u"netifaces is not available, it help discovering IPs, you can install it on https://pypi.python.org/pypi/netifaces")
35 netifaces = None
27 36
28 37
29 PLUGIN_INFO = { 38 PLUGIN_INFO = {
30 "name": "IP discovery", 39 "name": "IP discovery",
31 "import_name": "IP", 40 "import_name": "IP",
32 "type": C.PLUG_TYPE_MISC, 41 "type": C.PLUG_TYPE_MISC,
42 "recommendations": ["NAT-PORT"],
33 "main": "IPPlugin", 43 "main": "IPPlugin",
34 "handler": "no", 44 "handler": "no",
35 "description": _("""This plugin help to discover our external IP address.""") 45 "description": _("""This plugin help to discover our external IP address.""")
36 } 46 }
37 47
60 </category> 70 </category>
61 </general> 71 </general>
62 </params> 72 </params>
63 """.format(category=GET_IP_CATEGORY, name=GET_IP_NAME, label=GET_IP_LABEL) 73 """.format(category=GET_IP_CATEGORY, name=GET_IP_NAME, label=GET_IP_LABEL)
64 74
75
65 class IPPlugin(object): 76 class IPPlugin(object):
77 # TODO: refresh IP if a new connection is detected
78 # TODO: manage IPv6 when implemented in SàT
79 # TODO: implement XEP-0279
66 80
67 def __init__(self, host): 81 def __init__(self, host):
68 log.info(_("plugin IP discovery initialization")) 82 log.info(_("plugin IP discovery initialization"))
69 self.host = host 83 self.host = host
70 host.memory.updateParams(PARAMS) 84 host.memory.updateParams(PARAMS)
71 85
72 def profileConnected(self, profile): 86 # NAT-Port
73 client = self.host.getClient(profile) 87 try:
74 # XXX: we keep cache only for profile session as ip can change between them 88 self._nat = host.plugins['NAT-PORT']
75 client._ip_cache = None 89 except KeyError:
76 90 log.debug(u"NAT port plugin not available")
77 def getIP(self, profile): 91 self._nat = None
78 """Try to discover external IP 92
79 93 # XXX: cache is kept until SàT is restarted
94 # if IP may have changed, use self.refreshIP
95 self._ip_cache = None
96
97 def refreshIP(self):
98 # FIXME: use a trigger instead ?
99 self._ip_cache = None
100
101 def _externalAllowed(self, profile):
102 """Return value of parameter with autorisation of user to do external requests
103
104 if parameter is not set, a dialog is shown to use to get its confirmation, and parameted is set according to answer
80 @param profile: %(doc_profile)s 105 @param profile: %(doc_profile)s
81 @return (deferred): external IP address or None if it can't be discovered 106 @return (defer.Deferred[bool]): True if external request is autorised
82 """ 107 """
83 client = self.host.getClient(profile)
84 if client._ip_cache is not None:
85 return client._ip_cache
86
87 allow_get_ip = self.host.memory.params.getParamA(GET_IP_NAME, GET_IP_CATEGORY, use_default=False) 108 allow_get_ip = self.host.memory.params.getParamA(GET_IP_NAME, GET_IP_CATEGORY, use_default=False)
88 109
89 if allow_get_ip is None: 110 if allow_get_ip is None:
90 # we don't have autorisation from user yet to use get_ip, we ask him 111 # we don't have autorisation from user yet to use get_ip, we ask him
91 confirm_d = xml_tools.deferConfirm(self.host, _(GET_IP_CONFIRM), _(GET_IP_CONFIRM_TITLE), profile=profile)
92 def setParam(allowed): 112 def setParam(allowed):
93 # FIXME: we need to use boolConst as setParam only manage str/unicode 113 # FIXME: we need to use boolConst as setParam only manage str/unicode
94 # need to be fixed when params will be refactored 114 # need to be fixed when params will be refactored
95 self.host.memory.setParam(GET_IP_NAME, C.boolConst(allowed), GET_IP_CATEGORY) 115 self.host.memory.setParam(GET_IP_NAME, C.boolConst(allow_get_ip), GET_IP_CATEGORY)
96 return self.getIP(profile) 116 return allowed
97 117 d = xml_tools.deferConfirm(self.host, _(GET_IP_CONFIRM), _(GET_IP_CONFIRM_TITLE), profile=profile)
98 return confirm_d.addCallback(setParam) 118 d.addCallback(setParam)
99 119 return d
100 120
101 return getPage(GET_IP_PAGE) if allow_get_ip else None 121 return defer.succeed(allow_get_ip)
122
123 def _filterAddresse(self, ip_addr):
124 """Filter acceptable addresses
125
126 For now, just remove IPv4 local addresses
127 @param ip_addr(str): IP addresse
128 @return (bool): True if addresse is acceptable
129 """
130 return not ip_addr.startswith('127.')
131
132 def _insertFirst(self, addresses, ip_addr):
133 """Insert ip_addr as first item in addresses
134
135 @param ip_addr(str): IP addresse
136 @param addresses(list): list of IP addresses
137 """
138 if ip_addr in addresses:
139 if addresses[0] != ip_addr:
140 addresses.remove(ip_addr)
141 addresses.insert(0, ip_addr)
142 else:
143 addresses.insert(0, ip_addr)
144
145 def _getIPFromExternal(self, ext_url):
146 """Get local IP by doing a connection on an external url
147
148 @param ext_utl(str): url to connect to
149 @return (defer.Deferred): return local IP
150 """
151 url = urlparse.urlparse(ext_url)
152 port = url.port
153 if port is None:
154 if url.scheme=='http':
155 port = 80
156 elif url.scheme=='https':
157 port = 443
158 else:
159 log.error(u"Unknown url scheme: {}".format(url.scheme))
160 defer.returnValue(None)
161 if url.hostname is None:
162 log.error(u"Can't find url hostname for {}".format(GET_IP_PAGE))
163
164 point = endpoints.TCP4ClientEndpoint(reactor, url.hostname, port)
165 def gotConnection(p):
166 local_ip = p.transport.getHost().host
167 p.transport.loseConnection()
168 return local_ip
169
170 d = endpoints.connectProtocol(point, protocol.Protocol())
171 d.addCallback(gotConnection)
172 return d
173
174
175 @defer.inlineCallbacks
176 def getLocalIPs(self, profile):
177 """Try do discover local area network IPs
178
179 @param profile): %(doc_profile)s
180 @return (deferred): list of lan IP addresses
181 or empty list if it can't be discovered
182 if there are several addresses, the one used with the server is put first
183 """
184 # TODO: manage permission requesting (e.g. for UMTS link)
185 client = self.host.getClient(profile)
186 addresses = []
187
188 # we first try our luck with netifaces
189 if netifaces is not None:
190 addresses = []
191 for interface in netifaces.interfaces():
192 if_addresses = netifaces.ifaddresses(interface)
193 try:
194 inet_list = if_addresses[netifaces.AF_INET]
195 except KeyError:
196 continue
197 for data in inet_list:
198 addresse = data['addr']
199 if self._filterAddresse(addresse):
200 addresses.append(addresse)
201
202 # we first try with our connection to server
203 ip = client.xmlstream.transport.getHost().host
204 if self._filterAddresse(ip):
205 self._insertFirst(addresses, ip)
206 defer.returnValue(addresses)
207
208 # if not available, we try with NAT-Port
209 if self._nat is not None:
210 nat_ip = yield self._nat.getIP(local=True)
211 if nat_ip is not None:
212 self._insertFirst(addresses, nat_ip)
213 defer.returnValue(addresses)
214
215 if addresses:
216 defer.returnValue(addresses)
217
218 # still not luck, we need to contact external website
219 allow_get_ip = yield self._externalAllowed(profile)
220
221 if not allow_get_ip:
222 defer.returnValue(addresses)
223
224 ip_tuple = yield self._getIPFromExternal(GET_IP_PAGE)
225 self._insertFirst(addresses, ip_tuple.local)
226 defer.returnValue(addresses)
227
228
229 @defer.inlineCallbacks
230 def getExternalIP(self, profile):
231 """Try to discover external IP
232
233 @param profile: %(doc_profile)s
234 @return (deferred): external IP address or None if it can't be discovered
235 """
236 if self._ip_cache is not None:
237 defer.returnValue(self._ip_cache)
238
239 # we first try with NAT-Port
240 if self._nat is not None:
241 nat_ip = yield self._nat.getIP()
242 if nat_ip is not None:
243 defer.returnValue(nat_ip)
244
245 # then by requesting external website
246 allow_get_ip = yield self._externalAllowed(profile)
247 ip = webclient.getPage(GET_IP_PAGE) if allow_get_ip else None
248 defer.returnValue(ip)