Mercurial > libervia-backend
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 [] |