comparison sat/plugins/plugin_misc_ip.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents src/plugins/plugin_misc_ip.py@67cc54b01a12
children 56f94936df1e
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # SAT plugin for IP address discovery
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 from sat.core.i18n import _, D_
21 from sat.core.constants import Const as C
22 from sat.core.log import getLogger
23 log = getLogger(__name__)
24 from sat.tools import xml_tools
25 from twisted.web import client as webclient
26 from twisted.web import error as web_error
27 from twisted.internet import defer
28 from twisted.internet import reactor
29 from twisted.internet import protocol
30 from twisted.internet import endpoints
31 from twisted.internet import error as internet_error
32 from zope.interface import implements
33 from wokkel import disco, iwokkel
34 from twisted.words.protocols.jabber.xmlstream import XMPPHandler
35 from twisted.words.protocols.jabber.error import StanzaError
36 import urlparse
37 try:
38 import netifaces
39 except ImportError:
40 log.warning(u"netifaces is not available, it help discovering IPs, you can install it on https://pypi.python.org/pypi/netifaces")
41 netifaces = None
42
43
44 PLUGIN_INFO = {
45 C.PI_NAME: "IP discovery",
46 C.PI_IMPORT_NAME: "IP",
47 C.PI_TYPE: C.PLUG_TYPE_MISC,
48 C.PI_MODES: C.PLUG_MODE_BOTH,
49 C.PI_PROTOCOLS: ["XEP-0279"],
50 C.PI_RECOMMENDATIONS: ["NAT-PORT"],
51 C.PI_MAIN: "IPPlugin",
52 C.PI_HANDLER: "yes",
53 C.PI_DESCRIPTION: _("""This plugin help to discover our external IP address.""")
54 }
55
56 # TODO: GET_IP_PAGE should be configurable in sat.conf
57 GET_IP_PAGE = "http://salut-a-toi.org/whereami/" # This page must only return external IP of the requester
58 GET_IP_LABEL = D_(u"Allow external get IP")
59 GET_IP_CATEGORY = "General"
60 GET_IP_NAME = "allow_get_ip"
61 GET_IP_CONFIRM_TITLE = D_(u"Confirm external site request")
62 GET_IP_CONFIRM = D_(u"""To facilitate data transfer, we need to contact a website.
63 A request will be done on {page}
64 That means that administrators of {domain} can know that you use "{app_name}" and your IP Address.
65
66 IP address is an identifier to locate you on Internet (similar to a phone number).
67
68 Do you agree to do this request ?
69 """).format(
70 page = GET_IP_PAGE,
71 domain = urlparse.urlparse(GET_IP_PAGE).netloc,
72 app_name = C.APP_NAME)
73 NS_IP_CHECK = "urn:xmpp:sic:1"
74
75 PARAMS = """
76 <params>
77 <general>
78 <category name="{category}">
79 <param name="{name}" label="{label}" type="bool" />
80 </category>
81 </general>
82 </params>
83 """.format(category=GET_IP_CATEGORY, name=GET_IP_NAME, label=GET_IP_LABEL)
84
85
86 class IPPlugin(object):
87 # TODO: refresh IP if a new connection is detected
88 # TODO: manage IPv6 when implemented in SàT
89
90 def __init__(self, host):
91 log.info(_("plugin IP discovery initialization"))
92 self.host = host
93 host.memory.updateParams(PARAMS)
94
95 # NAT-Port
96 try:
97 self._nat = host.plugins['NAT-PORT']
98 except KeyError:
99 log.debug(u"NAT port plugin not available")
100 self._nat = None
101
102 # XXX: cache is kept until SàT is restarted
103 # if IP may have changed, use self.refreshIP
104 self._external_ip_cache = None
105 self._local_ip_cache = None
106
107 def getHandler(self, client):
108 return IPPlugin_handler()
109
110 def refreshIP(self):
111 # FIXME: use a trigger instead ?
112 self._external_ip_cache = None
113 self._local_ip_cache = None
114
115 def _externalAllowed(self, client):
116 """Return value of parameter with autorisation of user to do external requests
117
118 if parameter is not set, a dialog is shown to use to get its confirmation, and parameted is set according to answer
119 @return (defer.Deferred[bool]): True if external request is autorised
120 """
121 allow_get_ip = self.host.memory.params.getParamA(GET_IP_NAME, GET_IP_CATEGORY, use_default=False)
122
123 if allow_get_ip is None:
124 # we don't have autorisation from user yet to use get_ip, we ask him
125 def setParam(allowed):
126 # FIXME: we need to use boolConst as setParam only manage str/unicode
127 # need to be fixed when params will be refactored
128 self.host.memory.setParam(GET_IP_NAME, C.boolConst(allowed), GET_IP_CATEGORY)
129 return allowed
130 d = xml_tools.deferConfirm(self.host, _(GET_IP_CONFIRM), _(GET_IP_CONFIRM_TITLE), profile=client.profile)
131 d.addCallback(setParam)
132 return d
133
134 return defer.succeed(allow_get_ip)
135
136 def _filterAddresse(self, ip_addr):
137 """Filter acceptable addresses
138
139 For now, just remove IPv4 local addresses
140 @param ip_addr(str): IP addresse
141 @return (bool): True if addresse is acceptable
142 """
143 return not ip_addr.startswith('127.')
144
145 def _insertFirst(self, addresses, ip_addr):
146 """Insert ip_addr as first item in addresses
147
148 @param ip_addr(str): IP addresse
149 @param addresses(list): list of IP addresses
150 """
151 if ip_addr in addresses:
152 if addresses[0] != ip_addr:
153 addresses.remove(ip_addr)
154 addresses.insert(0, ip_addr)
155 else:
156 addresses.insert(0, ip_addr)
157
158 def _getIPFromExternal(self, ext_url):
159 """Get local IP by doing a connection on an external url
160
161 @param ext_utl(str): url to connect to
162 @return (D(str)): return local IP
163 """
164 url = urlparse.urlparse(ext_url)
165 port = url.port
166 if port is None:
167 if url.scheme=='http':
168 port = 80
169 elif url.scheme=='https':
170 port = 443
171 else:
172 log.error(u"Unknown url scheme: {}".format(url.scheme))
173 defer.returnValue(None)
174 if url.hostname is None:
175 log.error(u"Can't find url hostname for {}".format(GET_IP_PAGE))
176
177 point = endpoints.TCP4ClientEndpoint(reactor, url.hostname, port)
178 def gotConnection(p):
179 local_ip = p.transport.getHost().host
180 p.transport.loseConnection()
181 return local_ip
182
183 d = endpoints.connectProtocol(point, protocol.Protocol())
184 d.addCallback(gotConnection)
185 return d
186
187 @defer.inlineCallbacks
188 def getLocalIPs(self, client):
189 """Try do discover local area network IPs
190
191 @return (deferred): list of lan IP addresses
192 if there are several addresses, the one used with the server is put first
193 if no address is found, localhost IP will be in the list
194 """
195 # TODO: manage permission requesting (e.g. for UMTS link)
196 if self._local_ip_cache is not None:
197 defer.returnValue(self._local_ip_cache)
198 addresses = []
199 localhost = ['127.0.0.1']
200
201 # we first try our luck with netifaces
202 if netifaces is not None:
203 addresses = []
204 for interface in netifaces.interfaces():
205 if_addresses = netifaces.ifaddresses(interface)
206 try:
207 inet_list = if_addresses[netifaces.AF_INET]
208 except KeyError:
209 continue
210 for data in inet_list:
211 addresse = data['addr']
212 if self._filterAddresse(addresse):
213 addresses.append(addresse)
214
215 # then we use our connection to server
216 ip = client.xmlstream.transport.getHost().host
217 if self._filterAddresse(ip):
218 self._insertFirst(addresses, ip)
219 defer.returnValue(addresses)
220
221 # if server is local, we try with NAT-Port
222 if self._nat is not None:
223 nat_ip = yield self._nat.getIP(local=True)
224 if nat_ip is not None:
225 self._insertFirst(addresses, nat_ip)
226 defer.returnValue(addresses)
227
228 if addresses:
229 defer.returnValue(addresses)
230
231 # still not luck, we need to contact external website
232 allow_get_ip = yield self._externalAllowed(client)
233
234 if not allow_get_ip:
235 defer.returnValue(addresses or localhost)
236
237 try:
238 ip_tuple = yield self._getIPFromExternal(GET_IP_PAGE)
239 except (internet_error.DNSLookupError, internet_error.TimeoutError):
240 log.warning(u"Can't access Domain Name System")
241 defer.returnValue(addresses or localhost)
242 self._insertFirst(addresses, ip_tuple.local)
243 defer.returnValue(addresses)
244
245 @defer.inlineCallbacks
246 def getExternalIP(self, client):
247 """Try to discover external IP
248
249 @return (deferred): external IP address or None if it can't be discovered
250 """
251 if self._external_ip_cache is not None:
252 defer.returnValue(self._external_ip_cache)
253
254
255 # we first try with XEP-0279
256 ip_check = yield self.host.hasFeature(client, NS_IP_CHECK)
257 if ip_check:
258 log.debug(u"Server IP Check available, we use it to retrieve our IP")
259 iq_elt = client.IQ("get")
260 iq_elt.addElement((NS_IP_CHECK, 'address'))
261 try:
262 result_elt = yield iq_elt.send()
263 address_elt = result_elt.elements(NS_IP_CHECK, 'address').next()
264 ip_elt = address_elt.elements(NS_IP_CHECK,'ip').next()
265 except StopIteration:
266 log.warning(u"Server returned invalid result on XEP-0279 request, we ignore it")
267 except StanzaError as e:
268 log.warning(u"error while requesting ip to server: {}".format(e))
269 else:
270 # FIXME: server IP may not be the same as external IP (server can be on local machine or network)
271 # IP should be checked to see if we have a local one, and rejected in this case
272 external_ip = str(ip_elt)
273 log.debug(u"External IP found: {}".format(external_ip))
274 self._external_ip_cache = external_ip
275 defer.returnValue(self._external_ip_cache)
276
277 # then with NAT-Port
278 if self._nat is not None:
279 nat_ip = yield self._nat.getIP()
280 if nat_ip is not None:
281 self._external_ip_cache = nat_ip
282 defer.returnValue(nat_ip)
283
284 # and finally by requesting external website
285 allow_get_ip = yield self._externalAllowed(client)
286 try:
287 ip = (yield webclient.getPage(GET_IP_PAGE)) if allow_get_ip else None
288 except (internet_error.DNSLookupError, internet_error.TimeoutError):
289 log.warning(u"Can't access Domain Name System")
290 ip = None
291 except web_error.Error as e:
292 log.warning(u"Error while retrieving IP on {url}: {message}".format(url=GET_IP_PAGE, message=e))
293 ip = None
294 else:
295 self._external_ip_cache = ip
296 defer.returnValue(ip)
297
298
299 class IPPlugin_handler(XMPPHandler):
300 implements(iwokkel.IDisco)
301
302 def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
303 return [disco.DiscoFeature(NS_IP_CHECK)]
304
305 def getDiscoItems(self, requestor, target, nodeIdentifier=''):
306 return []