Mercurial > libervia-web
comparison libervia/server/server.py @ 1216:b2d067339de3
python 3 port:
/!\ Python 3.6+ is now needed to use libervia
/!\ instability may occur and features may not be working anymore, this will improve with time
/!\ TxJSONRPC dependency has been removed
The same procedure as in backend has been applied (check backend commit ab2696e34d29 logs
for details). Removed now deprecated code (Pyjamas compiled browser part, legacy blog,
JSON RPC related code).
Adapted code to work without `html` and `themes` dirs.
author | Goffi <goffi@goffi.org> |
---|---|
date | Tue, 13 Aug 2019 19:12:31 +0200 |
parents | a2df53dfbf46 |
children | fe9782391f63 |
comparison
equal
deleted
inserted
replaced
1215:f14ab8a25e8b | 1216:b2d067339de3 |
---|---|
16 | 16 |
17 # You should have received a copy of the GNU Affero General Public License | 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/>. | 18 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
19 | 19 |
20 import re | 20 import re |
21 import glob | |
22 import os.path | 21 import os.path |
23 import sys | 22 import sys |
24 import tempfile | 23 import urllib.parse |
25 import shutil | 24 import urllib.request, urllib.error |
26 import uuid | |
27 import urlparse | |
28 import urllib | |
29 import time | 25 import time |
30 import copy | 26 import copy |
31 from twisted.application import service | 27 from twisted.application import service |
32 from twisted.internet import reactor, defer, inotify | 28 from twisted.internet import reactor, defer, inotify |
33 from twisted.web import server | 29 from twisted.web import server |
34 from twisted.web import static | 30 from twisted.web import static |
35 from twisted.web import resource as web_resource | 31 from twisted.web import resource as web_resource |
36 from twisted.web import util as web_util | 32 from twisted.web import util as web_util |
37 from twisted.web import http | |
38 from twisted.web import vhost | 33 from twisted.web import vhost |
39 from twisted.python.components import registerAdapter | 34 from twisted.python.components import registerAdapter |
40 from twisted.python import failure | 35 from twisted.python import failure |
41 from twisted.python import filepath | 36 from twisted.python import filepath |
42 from twisted.words.protocols.jabber import jid | 37 from twisted.words.protocols.jabber import jid |
43 | |
44 from txjsonrpc.web import jsonrpc | |
45 from txjsonrpc import jsonrpclib | |
46 | 38 |
47 from sat.core.log import getLogger | 39 from sat.core.log import getLogger |
48 | 40 |
49 from sat_frontends.bridge.dbus_bridge import ( | 41 from sat_frontends.bridge.dbus_bridge import ( |
50 Bridge, | 42 Bridge, |
56 from sat.tools import utils | 48 from sat.tools import utils |
57 from sat.tools import config | 49 from sat.tools import config |
58 from sat.tools.common import regex | 50 from sat.tools.common import regex |
59 from sat.tools.common import template | 51 from sat.tools.common import template |
60 from sat.tools.common import uri as common_uri | 52 from sat.tools.common import uri as common_uri |
61 from httplib import HTTPS_PORT | |
62 import libervia | 53 import libervia |
63 from libervia.server import websockets | 54 from libervia.server import websockets |
64 from libervia.server.pages import LiberviaPage | 55 from libervia.server.pages import LiberviaPage |
65 from libervia.server.utils import quote, ProgressHandler | 56 from libervia.server.utils import quote, ProgressHandler |
66 from libervia.server.tasks import TasksManager | 57 from libervia.server.tasks import TasksManager |
71 from twisted.internet import ssl | 62 from twisted.internet import ssl |
72 except ImportError: | 63 except ImportError: |
73 ssl = None | 64 ssl = None |
74 | 65 |
75 from libervia.server.constants import Const as C | 66 from libervia.server.constants import Const as C |
76 from libervia.server.blog import MicroBlog | |
77 from libervia.server import session_iface | 67 from libervia.server import session_iface |
78 | 68 |
79 log = getLogger(__name__) | 69 log = getLogger(__name__) |
80 | 70 |
81 | 71 |
82 # following value are set from twisted.plugins.libervia_server initialise | 72 # following value are set from twisted.plugins.libervia_server initialise |
83 # (see the comment there) | 73 # (see the comment there) |
84 DATA_DIR_DEFAULT = OPT_PARAMETERS_BOTH = OPT_PARAMETERS_CFG = coerceDataDir = None | 74 DATA_DIR_DEFAULT = OPT_PARAMETERS_BOTH = OPT_PARAMETERS_CFG = None |
85 DEFAULT_MASK = (inotify.IN_CREATE | inotify.IN_MODIFY | inotify.IN_MOVE_SELF | 75 DEFAULT_MASK = (inotify.IN_CREATE | inotify.IN_MODIFY | inotify.IN_MOVE_SELF |
86 | inotify.IN_MOVED_TO) | 76 | inotify.IN_MOVED_TO) |
87 | 77 |
88 | 78 |
89 class FilesWatcher(object): | 79 class FilesWatcher(object): |
100 notifier.startReading() | 90 notifier.startReading() |
101 return self._notifier | 91 return self._notifier |
102 | 92 |
103 def watchDir(self, dir_path, callback, mask=DEFAULT_MASK, auto_add=False, | 93 def watchDir(self, dir_path, callback, mask=DEFAULT_MASK, auto_add=False, |
104 recursive=False, **kwargs): | 94 recursive=False, **kwargs): |
105 log.info(_(u"Watching directory {dir_path}").format(dir_path=dir_path)) | 95 log.info(_("Watching directory {dir_path}").format(dir_path=dir_path)) |
106 callbacks = [lambda __, filepath, mask: callback(self.host, filepath, | 96 callbacks = [lambda __, filepath, mask: callback(self.host, filepath, |
107 inotify.humanReadableMask(mask), **kwargs)] | 97 inotify.humanReadableMask(mask), **kwargs)] |
108 self.notifier.watch( | 98 self.notifier.watch( |
109 filepath.FilePath(dir_path), mask=mask, autoAdd=auto_add, recursive=recursive, | 99 filepath.FilePath(dir_path), mask=mask, autoAdd=auto_add, recursive=recursive, |
110 callbacks=callbacks) | 100 callbacks=callbacks) |
118 server.Session.__init__(self, *args, **kwargs) | 108 server.Session.__init__(self, *args, **kwargs) |
119 | 109 |
120 def lock(self): | 110 def lock(self): |
121 """Prevent session from expiring""" | 111 """Prevent session from expiring""" |
122 self.__lock = True | 112 self.__lock = True |
123 self._expireCall.reset(sys.maxint) | 113 self._expireCall.reset(sys.maxsize) |
124 | 114 |
125 def unlock(self): | 115 def unlock(self): |
126 """Allow session to expire again, and touch it""" | 116 """Allow session to expire again, and touch it""" |
127 self.__lock = False | 117 self.__lock = False |
128 self.touch() | 118 self.touch() |
144 super(ProtectedFile, self).__init__(*args, **kwargs) | 134 super(ProtectedFile, self).__init__(*args, **kwargs) |
145 | 135 |
146 def directoryListing(self): | 136 def directoryListing(self): |
147 return web_resource.NoResource() | 137 return web_resource.NoResource() |
148 | 138 |
139 | |
140 def getChild(self, path, request): | |
141 return super().getChild(path, request) | |
142 | |
143 def getChildWithDefault(self, path, request): | |
144 return super().getChildWithDefault(path, request) | |
145 | |
146 def getChildForRequest(self, request): | |
147 return super().getChildForRequest(request) | |
149 | 148 |
150 class LiberviaRootResource(ProtectedFile): | 149 class LiberviaRootResource(ProtectedFile): |
151 """Specialized resource for Libervia root | 150 """Specialized resource for Libervia root |
152 | 151 |
153 handle redirections declared in sat.conf | 152 handle redirections declared in sat.conf |
163 self.uri_callbacks = {} | 162 self.uri_callbacks = {} |
164 self.pages_redirects = {} | 163 self.pages_redirects = {} |
165 self.cached_urls = {} | 164 self.cached_urls = {} |
166 self.main_menu = None | 165 self.main_menu = None |
167 | 166 |
168 def __unicode__(self): | 167 def __str__(self): |
169 return (u"Root resource for {host_name} using {site_name} at {site_path} and " | 168 return ("Root resource for {host_name} using {site_name} at {site_path} and " |
170 u"deserving files at {path}".format( | 169 "deserving files at {path}".format( |
171 host_name=self.host_name, site_name=self.site_name, | 170 host_name=self.host_name, site_name=self.site_name, |
172 site_path=self.site_path, path=self.path)) | 171 site_path=self.site_path, path=self.path)) |
173 | |
174 def __str__(self): | |
175 return self.__unicode__.encode('utf-8') | |
176 | 172 |
177 def _initRedirections(self, options): | 173 def _initRedirections(self, options): |
178 url_redirections = options["url_redirections_dict"] | 174 url_redirections = options["url_redirections_dict"] |
179 | 175 |
180 url_redirections = url_redirections.get(self.site_name, {}) | 176 url_redirections = url_redirections.get(self.site_name, {}) |
181 | 177 |
182 ## redirections | 178 ## redirections |
183 self.redirections = {} | 179 self.redirections = {} |
184 self.inv_redirections = {} # new URL to old URL map | 180 self.inv_redirections = {} # new URL to old URL map |
185 | 181 |
186 for old, new_data in url_redirections.iteritems(): | 182 for old, new_data in url_redirections.items(): |
187 # new_data can be a dictionary or a unicode url | 183 # new_data can be a dictionary or a unicode url |
188 if isinstance(new_data, dict): | 184 if isinstance(new_data, dict): |
189 # new_data dict must contain either "url", "page" or "path" key | 185 # new_data dict must contain either "url", "page" or "path" key |
190 # (exclusive) | 186 # (exclusive) |
191 # if "path" is used, a file url is constructed with it | 187 # if "path" is used, a file url is constructed with it |
192 if len({"path", "url", "page"}.intersection(new_data.keys())) != 1: | 188 if len({"path", "url", "page"}.intersection(list(new_data.keys()))) != 1: |
193 raise ValueError( | 189 raise ValueError( |
194 u'You must have one and only one of "url", "page" or "path" key ' | 190 'You must have one and only one of "url", "page" or "path" key ' |
195 u'in your url_redirections_dict data') | 191 'in your url_redirections_dict data') |
196 if "url" in new_data: | 192 if "url" in new_data: |
197 new = new_data["url"] | 193 new = new_data["url"] |
198 elif "page" in new_data: | 194 elif "page" in new_data: |
199 new = new_data | 195 new = new_data |
200 new["type"] = "page" | 196 new["type"] = "page" |
201 new.setdefault("path_args", []) | 197 new.setdefault("path_args", []) |
202 if not isinstance(new["path_args"], list): | 198 if not isinstance(new["path_args"], list): |
203 log.error( | 199 log.error( |
204 _(u'"path_args" in redirection of {old} must be a list. ' | 200 _('"path_args" in redirection of {old} must be a list. ' |
205 u'Ignoring the redirection'.format(old=old))) | 201 'Ignoring the redirection'.format(old=old))) |
206 continue | 202 continue |
207 new.setdefault("query_args", {}) | 203 new.setdefault("query_args", {}) |
208 if not isinstance(new["query_args"], dict): | 204 if not isinstance(new["query_args"], dict): |
209 log.error( | 205 log.error( |
210 _( | 206 _( |
211 u'"query_args" in redirection of {old} must be a ' | 207 '"query_args" in redirection of {old} must be a ' |
212 u'dictionary. Ignoring the redirection'.format(old=old))) | 208 'dictionary. Ignoring the redirection'.format(old=old))) |
213 continue | 209 continue |
214 new["path_args"] = [quote(a) for a in new["path_args"]] | 210 new["path_args"] = [quote(a) for a in new["path_args"]] |
215 # we keep an inversed dict of page redirection | 211 # we keep an inversed dict of page redirection |
216 # (page/path_args => redirecting URL) | 212 # (page/path_args => redirecting URL) |
217 # so getURL can return the redirecting URL if the same arguments | 213 # so getURL can return the redirecting URL if the same arguments |
221 args_hash | 217 args_hash |
222 ] = old | 218 ] = old |
223 | 219 |
224 # we need lists in query_args because it will be used | 220 # we need lists in query_args because it will be used |
225 # as it in request.path_args | 221 # as it in request.path_args |
226 for k, v in new["query_args"].iteritems(): | 222 for k, v in new["query_args"].items(): |
227 if isinstance(v, basestring): | 223 if isinstance(v, str): |
228 new["query_args"][k] = [v] | 224 new["query_args"][k] = [v] |
229 elif "path" in new_data: | 225 elif "path" in new_data: |
230 new = "file:{}".format(urllib.quote(new_data["path"])) | 226 new = "file:{}".format(urllib.parse.quote(new_data["path"])) |
231 elif isinstance(new_data, basestring): | 227 elif isinstance(new_data, str): |
232 new = new_data | 228 new = new_data |
233 new_data = {} | 229 new_data = {} |
234 else: | 230 else: |
235 log.error( | 231 log.error( |
236 _(u"ignoring invalid redirection value: {new_data}").format( | 232 _("ignoring invalid redirection value: {new_data}").format( |
237 new_data=new_data | 233 new_data=new_data |
238 ) | 234 ) |
239 ) | 235 ) |
240 continue | 236 continue |
241 | 237 |
242 # some normalization | 238 # some normalization |
243 if not old.strip(): | 239 if not old.strip(): |
244 # root URL special case | 240 # root URL special case |
245 old = "" | 241 old = "" |
246 elif not old.startswith("/"): | 242 elif not old.startswith("/"): |
247 log.error(_(u"redirected url must start with '/', got {value}. Ignoring") | 243 log.error(_("redirected url must start with '/', got {value}. Ignoring") |
248 .format(value=old)) | 244 .format(value=old)) |
249 continue | 245 continue |
250 else: | 246 else: |
251 old = self._normalizeURL(old) | 247 old = self._normalizeURL(old) |
252 | 248 |
253 if isinstance(new, dict): | 249 if isinstance(new, dict): |
254 # dict are handled differently, they contain data | 250 # dict are handled differently, they contain data |
255 # which ared use dynamically when the request is done | 251 # which ared use dynamically when the request is done |
256 self.redirections[old] = new | 252 self.redirections[old] = new |
257 if not old: | 253 if not old: |
258 if new[u"type"] == u"page": | 254 if new["type"] == "page": |
259 log.info( | 255 log.info( |
260 _(u"Root URL redirected to page {name}").format( | 256 _("Root URL redirected to page {name}").format( |
261 name=new[u"page"] | 257 name=new["page"] |
262 ) | 258 ) |
263 ) | 259 ) |
264 else: | 260 else: |
265 if new[u"type"] == u"page": | 261 if new["type"] == "page": |
266 page = self.getPageByName(new[u"page"]) | 262 page = self.getPageByName(new["page"]) |
267 url = page.getURL(*new.get(u"path_args", [])) | 263 url = page.getURL(*new.get("path_args", [])) |
268 self.inv_redirections[url] = old | 264 self.inv_redirections[url] = old |
269 continue | 265 continue |
270 | 266 |
271 # at this point we have a redirection URL in new, we can parse it | 267 # at this point we have a redirection URL in new, we can parse it |
272 new_url = urlparse.urlsplit(new.encode("utf-8")) | 268 new_url = urllib.parse.urlsplit(new) |
273 | 269 |
274 # we handle the known URL schemes | 270 # we handle the known URL schemes |
275 if new_url.scheme == "xmpp": | 271 if new_url.scheme == "xmpp": |
276 location = self.getPagePathFromURI(new) | 272 location = self.getPagePathFromURI(new) |
277 if location is None: | 273 if location is None: |
278 log.warning( | 274 log.warning( |
279 _(u"ignoring redirection, no page found to handle this URI: " | 275 _("ignoring redirection, no page found to handle this URI: " |
280 u"{uri}").format(uri=new)) | 276 "{uri}").format(uri=new)) |
281 continue | 277 continue |
282 request_data = self._getRequestData(location) | 278 request_data = self._getRequestData(location) |
283 if old: | 279 if old: |
284 self.inv_redirections[location] = old | 280 self.inv_redirections[location] = old |
285 | 281 |
286 elif new_url.scheme in ("", "http", "https"): | 282 elif new_url.scheme in ("", "http", "https"): |
287 # direct redirection | 283 # direct redirection |
288 if new_url.netloc: | 284 if new_url.netloc: |
289 raise NotImplementedError( | 285 raise NotImplementedError( |
290 u"netloc ({netloc}) is not implemented yet for " | 286 "netloc ({netloc}) is not implemented yet for " |
291 u"url_redirections_dict, it is not possible to redirect to an " | 287 "url_redirections_dict, it is not possible to redirect to an " |
292 u"external website".format(netloc=new_url.netloc)) | 288 "external website".format(netloc=new_url.netloc)) |
293 location = urlparse.urlunsplit( | 289 location = urllib.parse.urlunsplit( |
294 ("", "", new_url.path, new_url.query, new_url.fragment) | 290 ("", "", new_url.path, new_url.query, new_url.fragment) |
295 ).decode("utf-8") | 291 ) |
296 request_data = self._getRequestData(location) | 292 request_data = self._getRequestData(location) |
297 if old: | 293 if old: |
298 self.inv_redirections[location] = old | 294 self.inv_redirections[location] = old |
299 | 295 |
300 elif new_url.scheme in ("file"): | 296 elif new_url.scheme == "file": |
301 # file or directory | 297 # file or directory |
302 if new_url.netloc: | 298 if new_url.netloc: |
303 raise NotImplementedError( | 299 raise NotImplementedError( |
304 u"netloc ({netloc}) is not implemented for url redirection to " | 300 "netloc ({netloc}) is not implemented for url redirection to " |
305 u"file system, it is not possible to redirect to an external " | 301 "file system, it is not possible to redirect to an external " |
306 "host".format( | 302 "host".format( |
307 netloc=new_url.netloc)) | 303 netloc=new_url.netloc)) |
308 path = urllib.unquote(new_url.path) | 304 path = urllib.parse.unquote(new_url.path) |
309 if not os.path.isabs(path): | 305 if not os.path.isabs(path): |
310 raise ValueError( | 306 raise ValueError( |
311 u"file redirection must have an absolute path: e.g. " | 307 "file redirection must have an absolute path: e.g. " |
312 u"file:/path/to/my/file") | 308 "file:/path/to/my/file") |
313 # for file redirection, we directly put child here | 309 # for file redirection, we directly put child here |
314 segments, __, last_segment = old.rpartition("/") | 310 segments, __, last_segment = old.rpartition("/") |
315 url_segments = segments.split("/") if segments else [] | 311 url_segments = segments.split("/") if segments else [] |
316 current = self | 312 current = self |
317 for segment in url_segments: | 313 for segment in url_segments: |
320 current = resource | 316 current = resource |
321 resource_class = ( | 317 resource_class = ( |
322 ProtectedFile if new_data.get("protected", True) else static.File | 318 ProtectedFile if new_data.get("protected", True) else static.File |
323 ) | 319 ) |
324 current.putChild( | 320 current.putChild( |
325 last_segment, | 321 last_segment.encode('utf-8'), |
326 resource_class(path, defaultType="application/octet-stream") | 322 resource_class(path, defaultType="application/octet-stream") |
327 ) | 323 ) |
328 log.info(u"[{host_name}] Added redirection from /{old} to file system " | 324 log.info("[{host_name}] Added redirection from /{old} to file system " |
329 u"path {path}".format(host_name=self.host_name, | 325 "path {path}".format(host_name=self.host_name, |
330 old=old.decode("utf-8"), | 326 old=old, |
331 path=path.decode("utf-8"))) | 327 path=path)) |
332 continue # we don't want to use redirection system, so we continue here | 328 continue # we don't want to use redirection system, so we continue here |
333 | 329 |
334 else: | 330 else: |
335 raise NotImplementedError( | 331 raise NotImplementedError( |
336 u"{scheme}: scheme is not managed for url_redirections_dict".format( | 332 "{scheme}: scheme is not managed for url_redirections_dict".format( |
337 scheme=new_url.scheme | 333 scheme=new_url.scheme |
338 ) | 334 ) |
339 ) | 335 ) |
340 | 336 |
341 self.redirections[old] = request_data | 337 self.redirections[old] = request_data |
342 if not old: | 338 if not old: |
343 log.info(_(u"[{host_name}] Root URL redirected to {uri}") | 339 log.info(_("[{host_name}] Root URL redirected to {uri}") |
344 .format(host_name=self.host_name, | 340 .format(host_name=self.host_name, |
345 uri=request_data[1].decode("utf-8"))) | 341 uri=request_data[1])) |
346 | 342 |
347 # the default root URL, if not redirected | 343 # the default root URL, if not redirected |
348 if not "" in self.redirections: | 344 if not "" in self.redirections: |
349 self.redirections[""] = self._getRequestData(C.LIBERVIA_PAGE_START) | 345 self.redirections[""] = self._getRequestData(C.LIBERVIA_PAGE_START) |
350 | 346 |
351 def _setMenu(self, menus): | 347 def _setMenu(self, menus): |
352 menus = menus.get(self.site_name, []) | 348 menus = menus.get(self.site_name, []) |
353 main_menu = [] | 349 main_menu = [] |
354 for menu in menus: | 350 for menu in menus: |
355 if not menu: | 351 if not menu: |
356 msg = _(u"menu item can't be empty") | 352 msg = _("menu item can't be empty") |
357 log.error(msg) | 353 log.error(msg) |
358 raise ValueError(msg) | 354 raise ValueError(msg) |
359 elif isinstance(menu, list): | 355 elif isinstance(menu, list): |
360 if len(menu) != 2: | 356 if len(menu) != 2: |
361 msg = _( | 357 msg = _( |
362 u"menu item as list must be in the form [page_name, absolue URL]" | 358 "menu item as list must be in the form [page_name, absolue URL]" |
363 ) | 359 ) |
364 log.error(msg) | 360 log.error(msg) |
365 raise ValueError(msg) | 361 raise ValueError(msg) |
366 page_name, url = menu | 362 page_name, url = menu |
367 else: | 363 else: |
368 page_name = menu | 364 page_name = menu |
369 try: | 365 try: |
370 url = self.getPageByName(page_name).url | 366 url = self.getPageByName(page_name).url |
371 except KeyError as e: | 367 except KeyError as e: |
372 log_msg = _(u"Can'find a named page ({msg}), please check " | 368 log_msg = _("Can'find a named page ({msg}), please check " |
373 u"menu_json in configuration.").format(msg=e.args[0]) | 369 "menu_json in configuration.").format(msg=e.args[0]) |
374 log.error(log_msg) | 370 log.error(log_msg) |
375 raise exceptions.ConfigError(log_msg) | 371 raise exceptions.ConfigError(log_msg) |
376 main_menu.append((page_name, url)) | 372 main_menu.append((page_name, url)) |
377 self.main_menu = main_menu | 373 self.main_menu = main_menu |
378 | 374 |
383 @param lower(bool): lower case of url if True | 379 @param lower(bool): lower case of url if True |
384 @return (str): normalized URL | 380 @return (str): normalized URL |
385 """ | 381 """ |
386 if lower: | 382 if lower: |
387 url = url.lower() | 383 url = url.lower() |
388 return "/".join((p for p in url.encode("utf-8").split("/") if p)) | 384 return "/".join((p for p in url.split("/") if p)) |
389 | 385 |
390 def _getRequestData(self, uri): | 386 def _getRequestData(self, uri): |
391 """Return data needed to redirect request | 387 """Return data needed to redirect request |
392 | 388 |
393 @param url(unicode): destination url | 389 @param url(unicode): destination url |
395 splitted path as in Request.postpath | 391 splitted path as in Request.postpath |
396 uri as in Request.uri | 392 uri as in Request.uri |
397 path as in Request.path | 393 path as in Request.path |
398 args as in Request.args | 394 args as in Request.args |
399 """ | 395 """ |
400 uri = uri.encode("utf-8") | 396 uri = uri |
401 # XXX: we reuse code from twisted.web.http.py here | 397 # XXX: we reuse code from twisted.web.http.py here |
402 # as we need to have the same behaviour | 398 # as we need to have the same behaviour |
403 x = uri.split(b"?", 1) | 399 x = uri.split("?", 1) |
404 | 400 |
405 if len(x) == 1: | 401 if len(x) == 1: |
406 path = uri | 402 path = uri |
407 args = {} | 403 args = {} |
408 else: | 404 else: |
409 path, argstring = x | 405 path, argstring = x |
410 args = http.parse_qs(argstring, 1) | 406 args = urllib.parse.parse_qs(argstring, True) |
411 | 407 |
412 # XXX: splitted path case must not be changed, as it may be significant | 408 # XXX: splitted path case must not be changed, as it may be significant |
413 # (e.g. for blog items) | 409 # (e.g. for blog items) |
414 return ( | 410 return ( |
415 self._normalizeURL(path.decode("utf-8"), lower=False).split("/"), | 411 self._normalizeURL(path, lower=False).split("/"), |
416 uri, | 412 uri, |
417 path, | 413 path, |
418 args, | 414 args, |
419 ) | 415 ) |
420 | 416 |
433 pass | 429 pass |
434 else: | 430 else: |
435 try: | 431 try: |
436 __, uri, __, __ = request_data | 432 __, uri, __, __ = request_data |
437 except ValueError: | 433 except ValueError: |
438 uri = u"" | 434 uri = "" |
439 log.error(D_( u"recursive redirection, please fix this URL:\n" | 435 log.error(D_( "recursive redirection, please fix this URL:\n" |
440 u"{old} ==> {new}").format( | 436 "{old} ==> {new}").format( |
441 old=request.uri.decode("utf-8"), new=uri.decode("utf-8"))) | 437 old=request.uri.decode("utf-8"), new=uri)) |
442 return web_resource.NoResource() | 438 return web_resource.NoResource() |
443 | 439 |
444 request._redirected = True # here to avoid recursive redirections | 440 request._redirected = True # here to avoid recursive redirections |
445 | 441 |
446 if isinstance(request_data, dict): | 442 if isinstance(request_data, dict): |
448 try: | 444 try: |
449 page = self.getPageByName(request_data["page"]) | 445 page = self.getPageByName(request_data["page"]) |
450 except KeyError: | 446 except KeyError: |
451 log.error( | 447 log.error( |
452 _( | 448 _( |
453 u'Can\'t find page named "{name}" requested in redirection' | 449 'Can\'t find page named "{name}" requested in redirection' |
454 ).format(name=request_data["page"]) | 450 ).format(name=request_data["page"]) |
455 ) | 451 ) |
456 return web_resource.NoResource() | 452 return web_resource.NoResource() |
457 request.postpath = request_data["path_args"][:] + request.postpath | 453 path_args = [pa.encode('utf-8') for pa in request_data["path_args"]] |
454 request.postpath = path_args + request.postpath | |
458 | 455 |
459 try: | 456 try: |
460 request.args.update(request_data["query_args"]) | 457 request.args.update(request_data["query_args"]) |
461 except (TypeError, ValueError): | 458 except (TypeError, ValueError): |
462 log.error( | 459 log.error( |
463 _(u"Invalid args in redirection: {query_args}").format( | 460 _("Invalid args in redirection: {query_args}").format( |
464 query_args=request_data["query_args"] | 461 query_args=request_data["query_args"] |
465 ) | 462 ) |
466 ) | 463 ) |
467 return web_resource.NoResource() | 464 return web_resource.NoResource() |
468 return page | 465 return page |
469 else: | 466 else: |
470 raise exceptions.InternalError(u"unknown request_data type") | 467 raise exceptions.InternalError("unknown request_data type") |
471 else: | 468 else: |
472 path_list, uri, path, args = request_data | 469 path_list, uri, path, args = request_data |
470 path_list = [p.encode('utf-8') for p in path_list] | |
473 log.debug( | 471 log.debug( |
474 u"Redirecting URL {old} to {new}".format( | 472 "Redirecting URL {old} to {new}".format( |
475 old=request.uri.decode("utf-8"), new=uri.decode("utf-8") | 473 old=request.uri.decode('utf-8'), new=uri |
476 ) | 474 ) |
477 ) | 475 ) |
478 # we change the request to reflect the new url | 476 # we change the request to reflect the new url |
479 request.postpath = path_list[1:] + request.postpath | 477 request.postpath = path_list[1:] + request.postpath |
480 request.args.update(args) | 478 request.args.update(args) |
518 return url | 516 return url |
519 | 517 |
520 def getChildWithDefault(self, name, request): | 518 def getChildWithDefault(self, name, request): |
521 # XXX: this method is overriden only for root url | 519 # XXX: this method is overriden only for root url |
522 # which is the only ones who need to be handled before other children | 520 # which is the only ones who need to be handled before other children |
523 if name == "" and not request.postpath: | 521 if name == b"" and not request.postpath: |
524 return self._redirect(request, self.redirections[""]) | 522 return self._redirect(request, self.redirections[""]) |
525 return super(LiberviaRootResource, self).getChildWithDefault(name, request) | 523 return super(LiberviaRootResource, self).getChildWithDefault(name, request) |
526 | 524 |
527 def getChild(self, name, request): | 525 def getChild(self, name, request): |
528 resource = super(LiberviaRootResource, self).getChild(name, request) | 526 resource = super(LiberviaRootResource, self).getChild(name, request) |
529 | 527 |
530 if isinstance(resource, web_resource.NoResource): | 528 if isinstance(resource, web_resource.NoResource): |
531 # if nothing was found, we try our luck with redirections | 529 # if nothing was found, we try our luck with redirections |
532 # XXX: we want redirections to happen only if everything else failed | 530 # XXX: we want redirections to happen only if everything else failed |
533 path_elt = request.prepath + request.postpath | 531 path_elt = request.prepath + request.postpath |
534 for idx in xrange(len(path_elt), 0, -1): | 532 for idx in range(len(path_elt), 0, -1): |
535 test_url = "/".join(path_elt[:idx]).lower() | 533 test_url = b"/".join(path_elt[:idx]).decode('utf-8').lower() |
536 if test_url in self.redirections: | 534 if test_url in self.redirections: |
537 request_data = self.redirections[test_url] | 535 request_data = self.redirections[test_url] |
538 request.postpath = path_elt[idx:] | 536 request.postpath = path_elt[idx:] |
539 return self._redirect(request, request_data) | 537 return self._redirect(request, request_data) |
540 | 538 |
541 return resource | 539 return resource |
542 | 540 |
543 def putChild(self, path, resource): | 541 def putChild(self, path, resource): |
544 """Add a child to the root resource""" | 542 """Add a child to the root resource""" |
543 if not isinstance(path, bytes): | |
544 raise ValueError("path must be specified in bytes") | |
545 if not isinstance(resource, web_resource.EncodingResourceWrapper): | 545 if not isinstance(resource, web_resource.EncodingResourceWrapper): |
546 # FIXME: check that no information is leaked (c.f. https://twistedmatrix.com/documents/current/web/howto/using-twistedweb.html#request-encoders) | 546 # FIXME: check that no information is leaked (c.f. https://twistedmatrix.com/documents/current/web/howto/using-twistedweb.html#request-encoders) |
547 resource = web_resource.EncodingResourceWrapper( | 547 resource = web_resource.EncodingResourceWrapper( |
548 resource, [server.GzipEncoderFactory()]) | 548 resource, [server.GzipEncoderFactory()]) |
549 | 549 |
560 f.indexNames = self.indexNames[:] | 560 f.indexNames = self.indexNames[:] |
561 f.childNotFound = self.childNotFound | 561 f.childNotFound = self.childNotFound |
562 return f | 562 return f |
563 | 563 |
564 | 564 |
565 class JSONRPCMethodManager(jsonrpc.JSONRPC): | |
566 def __init__(self, sat_host): | |
567 jsonrpc.JSONRPC.__init__(self) | |
568 self.sat_host = sat_host | |
569 | |
570 def _bridgeCallEb(self, failure_): | |
571 """Raise a jsonrpclib failure for the frontend""" | |
572 return failure.Failure( | |
573 jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, failure_.value.classname) | |
574 ) | |
575 | |
576 def asyncBridgeCall(self, method_name, *args, **kwargs): | |
577 d = self.sat_host.bridgeCall(method_name, *args, **kwargs) | |
578 d.addErrback(self._bridgeCallEb) | |
579 return d | |
580 | |
581 | |
582 class MethodHandler(JSONRPCMethodManager): | |
583 def __init__(self, sat_host): | |
584 JSONRPCMethodManager.__init__(self, sat_host) | |
585 | |
586 def render(self, request): | |
587 self.session = request.getSession() | |
588 profile = session_iface.ISATSession(self.session).profile | |
589 if not profile: | |
590 # user is not identified, we return a jsonrpc fault | |
591 parsed = jsonrpclib.loads(request.content.read()) | |
592 fault = jsonrpclib.Fault( | |
593 C.ERRNUM_LIBERVIA, C.NOT_ALLOWED | |
594 ) # FIXME: define some standard error codes for libervia | |
595 return jsonrpc.JSONRPC._cbRender( | |
596 self, fault, request, parsed.get("id"), parsed.get("jsonrpc") | |
597 ) # pylint: disable=E1103 | |
598 return jsonrpc.JSONRPC.render(self, request) | |
599 | |
600 @defer.inlineCallbacks | |
601 def jsonrpc_getVersion(self): | |
602 """Return SàT version""" | |
603 try: | |
604 defer.returnValue(self._version_cache) | |
605 except AttributeError: | |
606 self._version_cache = yield self.sat_host.bridgeCall("getVersion") | |
607 defer.returnValue(self._version_cache) | |
608 | |
609 def jsonrpc_getLiberviaVersion(self): | |
610 """Return Libervia version""" | |
611 return self.sat_host.full_version | |
612 | |
613 def jsonrpc_disconnect(self): | |
614 """Disconnect the profile""" | |
615 sat_session = session_iface.ISATSession(self.session) | |
616 profile = sat_session.profile | |
617 self.sat_host.bridgeCall("disconnect", profile) | |
618 | |
619 def jsonrpc_getContacts(self): | |
620 """Return all passed args.""" | |
621 profile = session_iface.ISATSession(self.session).profile | |
622 return self.sat_host.bridgeCall("getContacts", profile) | |
623 | |
624 @defer.inlineCallbacks | |
625 def jsonrpc_addContact(self, entity, name, groups): | |
626 """Subscribe to contact presence, and add it to the given groups""" | |
627 profile = session_iface.ISATSession(self.session).profile | |
628 yield self.sat_host.bridgeCall("addContact", entity, profile) | |
629 yield self.sat_host.bridgeCall("updateContact", entity, name, groups, profile) | |
630 | |
631 def jsonrpc_delContact(self, entity): | |
632 """Remove contact from contacts list""" | |
633 profile = session_iface.ISATSession(self.session).profile | |
634 return self.sat_host.bridgeCall("delContact", entity, profile) | |
635 | |
636 def jsonrpc_updateContact(self, entity, name, groups): | |
637 """Update contact's roster item""" | |
638 profile = session_iface.ISATSession(self.session).profile | |
639 return self.sat_host.bridgeCall("updateContact", entity, name, groups, profile) | |
640 | |
641 def jsonrpc_subscription(self, sub_type, entity): | |
642 """Confirm (or infirm) subscription, | |
643 and setup user roster in case of subscription""" | |
644 profile = session_iface.ISATSession(self.session).profile | |
645 return self.sat_host.bridgeCall("subscription", sub_type, entity, profile) | |
646 | |
647 def jsonrpc_getWaitingSub(self): | |
648 """Return list of room already joined by user""" | |
649 profile = session_iface.ISATSession(self.session).profile | |
650 return self.sat_host.bridgeCall("getWaitingSub", profile) | |
651 | |
652 def jsonrpc_setStatus(self, presence, status): | |
653 """Change the presence and/or status | |
654 @param presence: value from ("", "chat", "away", "dnd", "xa") | |
655 @param status: any string to describe your status | |
656 """ | |
657 profile = session_iface.ISATSession(self.session).profile | |
658 return self.sat_host.bridgeCall( | |
659 "setPresence", "", presence, {"": status}, profile | |
660 ) | |
661 | |
662 def jsonrpc_messageSend(self, to_jid, msg, subject, type_, extra={}): | |
663 """send message""" | |
664 profile = session_iface.ISATSession(self.session).profile | |
665 return self.asyncBridgeCall( | |
666 "messageSend", to_jid, msg, subject, type_, extra, profile | |
667 ) | |
668 | |
669 ## PubSub ## | |
670 | |
671 def jsonrpc_psNodeDelete(self, service, node): | |
672 """Delete a whole node | |
673 | |
674 @param service (unicode): service jid | |
675 @param node (unicode): node to delete | |
676 """ | |
677 profile = session_iface.ISATSession(self.session).profile | |
678 return self.asyncBridgeCall("psNodeDelete", service, node, profile) | |
679 | |
680 # def jsonrpc_psRetractItem(self, service, node, item, notify): | |
681 # """Delete a whole node | |
682 | |
683 # @param service (unicode): service jid | |
684 # @param node (unicode): node to delete | |
685 # @param items (iterable): id of item to retract | |
686 # @param notify (bool): True if notification is required | |
687 # """ | |
688 # profile = session_iface.ISATSession(self.session).profile | |
689 # return self.asyncBridgeCall("psRetractItem", service, node, item, notify, | |
690 # profile) | |
691 | |
692 # def jsonrpc_psRetractItems(self, service, node, items, notify): | |
693 # """Delete a whole node | |
694 | |
695 # @param service (unicode): service jid | |
696 # @param node (unicode): node to delete | |
697 # @param items (iterable): ids of items to retract | |
698 # @param notify (bool): True if notification is required | |
699 # """ | |
700 # profile = session_iface.ISATSession(self.session).profile | |
701 # return self.asyncBridgeCall("psRetractItems", service, node, items, notify, | |
702 # profile) | |
703 | |
704 ## microblogging ## | |
705 | |
706 def jsonrpc_mbSend(self, service, node, mb_data): | |
707 """Send microblog data | |
708 | |
709 @param service (unicode): service jid or empty string to use profile's microblog | |
710 @param node (unicode): publishing node, or empty string to use microblog node | |
711 @param mb_data(dict): microblog data | |
712 @return: a deferred | |
713 """ | |
714 profile = session_iface.ISATSession(self.session).profile | |
715 return self.asyncBridgeCall("mbSend", service, node, mb_data, profile) | |
716 | |
717 def jsonrpc_mbRetract(self, service, node, items): | |
718 """Delete a whole node | |
719 | |
720 @param service (unicode): service jid, empty string for PEP | |
721 @param node (unicode): node to delete, empty string for default node | |
722 @param items (iterable): ids of items to retract | |
723 """ | |
724 profile = session_iface.ISATSession(self.session).profile | |
725 return self.asyncBridgeCall("mbRetract", service, node, items, profile) | |
726 | |
727 def jsonrpc_mbGet(self, service_jid, node, max_items, item_ids, extra): | |
728 """Get last microblogs from publisher_jid | |
729 | |
730 @param service_jid (unicode): pubsub service, usually publisher jid | |
731 @param node(unicode): mblogs node, or empty string to get the defaut one | |
732 @param max_items (int): maximum number of item to get or C.NO_LIMIT to get | |
733 everything | |
734 @param item_ids (list[unicode]): list of item IDs | |
735 @param rsm (dict): TODO | |
736 @return: a deferred couple with the list of items and metadatas. | |
737 """ | |
738 profile = session_iface.ISATSession(self.session).profile | |
739 return self.asyncBridgeCall( | |
740 "mbGet", service_jid, node, max_items, item_ids, extra, profile | |
741 ) | |
742 | |
743 def jsonrpc_mbGetFromMany(self, publishers_type, publishers, max_items, extra): | |
744 """Get many blog nodes at once | |
745 | |
746 @param publishers_type (unicode): one of "ALL", "GROUP", "JID" | |
747 @param publishers (tuple(unicode)): tuple of publishers (empty list for all, | |
748 list of groups or list of jids) | |
749 @param max_items (int): maximum number of item to get or C.NO_LIMIT to get | |
750 everything | |
751 @param extra (dict): TODO | |
752 @return (str): RT Deferred session id | |
753 """ | |
754 profile = session_iface.ISATSession(self.session).profile | |
755 return self.sat_host.bridgeCall( | |
756 "mbGetFromMany", publishers_type, publishers, max_items, extra, profile | |
757 ) | |
758 | |
759 def jsonrpc_mbGetFromManyRTResult(self, rt_session): | |
760 """Get results from RealTime mbGetFromMany session | |
761 | |
762 @param rt_session (str): RT Deferred session id | |
763 """ | |
764 profile = session_iface.ISATSession(self.session).profile | |
765 return self.asyncBridgeCall("mbGetFromManyRTResult", rt_session, profile) | |
766 | |
767 def jsonrpc_mbGetFromManyWithComments( | |
768 self, | |
769 publishers_type, | |
770 publishers, | |
771 max_items, | |
772 max_comments, | |
773 rsm_dict, | |
774 rsm_comments_dict, | |
775 ): | |
776 """Helper method to get the microblogs and their comments in one shot | |
777 | |
778 @param publishers_type (str): type of the list of publishers (one of "GROUP" or | |
779 "JID" or "ALL") | |
780 @param publishers (list): list of publishers, according to publishers_type | |
781 (list of groups or list of jids) | |
782 @param max_items (int): optional limit on the number of retrieved items. | |
783 @param max_comments (int): maximum number of comments to retrieve | |
784 @param rsm_dict (dict): RSM data for initial items only | |
785 @param rsm_comments_dict (dict): RSM data for comments only | |
786 @param profile_key: profile key | |
787 @return (str): RT Deferred session id | |
788 """ | |
789 profile = session_iface.ISATSession(self.session).profile | |
790 return self.sat_host.bridgeCall( | |
791 "mbGetFromManyWithComments", | |
792 publishers_type, | |
793 publishers, | |
794 max_items, | |
795 max_comments, | |
796 rsm_dict, | |
797 rsm_comments_dict, | |
798 profile, | |
799 ) | |
800 | |
801 def jsonrpc_mbGetFromManyWithCommentsRTResult(self, rt_session): | |
802 """Get results from RealTime mbGetFromManyWithComments session | |
803 | |
804 @param rt_session (str): RT Deferred session id | |
805 """ | |
806 profile = session_iface.ISATSession(self.session).profile | |
807 return self.asyncBridgeCall( | |
808 "mbGetFromManyWithCommentsRTResult", rt_session, profile | |
809 ) | |
810 | |
811 # def jsonrpc_sendMblog(self, type_, dest, text, extra={}): | |
812 # """ Send microblog message | |
813 # @param type_ (unicode): one of "PUBLIC", "GROUP" | |
814 # @param dest (tuple(unicode)): recipient groups (ignored for "PUBLIC") | |
815 # @param text (unicode): microblog's text | |
816 # """ | |
817 # profile = session_iface.ISATSession(self.session).profile | |
818 # extra['allow_comments'] = 'True' | |
819 | |
820 # if not type_: # auto-detect | |
821 # type_ = "PUBLIC" if dest == [] else "GROUP" | |
822 | |
823 # if type_ in ("PUBLIC", "GROUP") and text: | |
824 # if type_ == "PUBLIC": | |
825 # #This text if for the public microblog | |
826 # log.debug("sending public blog") | |
827 # return self.sat_host.bridge.sendGroupBlog("PUBLIC", (), text, extra, | |
828 # profile) | |
829 # else: | |
830 # log.debug("sending group blog") | |
831 # dest = dest if isinstance(dest, list) else [dest] | |
832 # return self.sat_host.bridge.sendGroupBlog("GROUP", dest, text, extra, | |
833 # profile) | |
834 # else: | |
835 # raise Exception("Invalid data") | |
836 | |
837 # def jsonrpc_deleteMblog(self, pub_data, comments): | |
838 # """Delete a microblog node | |
839 # @param pub_data: a tuple (service, comment node identifier, item identifier) | |
840 # @param comments: comments node identifier (for main item) or False | |
841 # """ | |
842 # profile = session_iface.ISATSession(self.session).profile | |
843 # return self.sat_host.bridge.deleteGroupBlog(pub_data, comments if comments | |
844 # else '', profile) | |
845 | |
846 # def jsonrpc_updateMblog(self, pub_data, comments, message, extra={}): | |
847 # """Modify a microblog node | |
848 # @param pub_data: a tuple (service, comment node identifier, item identifier) | |
849 # @param comments: comments node identifier (for main item) or False | |
850 # @param message: new message | |
851 # @param extra: dict which option name as key, which can be: | |
852 # - allow_comments: True to accept an other level of comments, False else | |
853 # (default: False) | |
854 # - rich: if present, contain rich text in currently selected syntax | |
855 # """ | |
856 # profile = session_iface.ISATSession(self.session).profile | |
857 # if comments: | |
858 # extra['allow_comments'] = 'True' | |
859 # return self.sat_host.bridge.updateGroupBlog(pub_data, comments if comments | |
860 # else '', message, extra, profile) | |
861 | |
862 # def jsonrpc_sendMblogComment(self, node, text, extra={}): | |
863 # """ Send microblog message | |
864 # @param node: url of the comments node | |
865 # @param text: comment | |
866 # """ | |
867 # profile = session_iface.ISATSession(self.session).profile | |
868 # if node and text: | |
869 # return self.sat_host.bridge.sendGroupBlogComment(node, text, extra, profile) | |
870 # else: | |
871 # raise Exception("Invalid data") | |
872 | |
873 # def jsonrpc_getMblogs(self, publisher_jid, item_ids, max_items=C.RSM_MAX_ITEMS): | |
874 # """Get specified microblogs posted by a contact | |
875 # @param publisher_jid: jid of the publisher | |
876 # @param item_ids: list of microblogs items IDs | |
877 # @return list of microblog data (dict)""" | |
878 # profile = session_iface.ISATSession(self.session).profile | |
879 # d = self.asyncBridgeCall("getGroupBlogs", publisher_jid, item_ids, {'max_': unicode(max_items)}, False, profile) | |
880 # return d | |
881 | |
882 # def jsonrpc_getMblogsWithComments(self, publisher_jid, item_ids, max_comments=C.RSM_MAX_COMMENTS): | |
883 # """Get specified microblogs posted by a contact and their comments | |
884 # @param publisher_jid: jid of the publisher | |
885 # @param item_ids: list of microblogs items IDs | |
886 # @return list of couple (microblog data, list of microblog data)""" | |
887 # profile = session_iface.ISATSession(self.session).profile | |
888 # d = self.asyncBridgeCall("getGroupBlogsWithComments", publisher_jid, item_ids, {}, max_comments, profile) | |
889 # return d | |
890 | |
891 # def jsonrpc_getMassiveMblogs(self, publishers_type, publishers, rsm=None): | |
892 # """Get lasts microblogs posted by several contacts at once | |
893 | |
894 # @param publishers_type (unicode): one of "ALL", "GROUP", "JID" | |
895 # @param publishers (tuple(unicode)): tuple of publishers (empty list for all, list of groups or list of jids) | |
896 # @param rsm (dict): TODO | |
897 # @return: dict{unicode: list[dict]) | |
898 # key: publisher's jid | |
899 # value: list of microblog data (dict) | |
900 # """ | |
901 # profile = session_iface.ISATSession(self.session).profile | |
902 # if rsm is None: | |
903 # rsm = {'max_': unicode(C.RSM_MAX_ITEMS)} | |
904 # d = self.asyncBridgeCall("getMassiveGroupBlogs", publishers_type, publishers, rsm, profile) | |
905 # self.sat_host.bridge.massiveSubscribeGroupBlogs(publishers_type, publishers, profile) | |
906 # return d | |
907 | |
908 # def jsonrpc_getMblogComments(self, service, node, rsm=None): | |
909 # """Get all comments of given node | |
910 # @param service: jid of the service hosting the node | |
911 # @param node: comments node | |
912 # """ | |
913 # profile = session_iface.ISATSession(self.session).profile | |
914 # if rsm is None: | |
915 # rsm = {'max_': unicode(C.RSM_MAX_COMMENTS)} | |
916 # d = self.asyncBridgeCall("getGroupBlogComments", service, node, rsm, profile) | |
917 # return d | |
918 | |
919 def jsonrpc_getPresenceStatuses(self): | |
920 """Get Presence information for connected contacts""" | |
921 profile = session_iface.ISATSession(self.session).profile | |
922 return self.sat_host.bridgeCall("getPresenceStatuses", profile) | |
923 | |
924 def jsonrpc_historyGet(self, from_jid, to_jid, size, between, search=""): | |
925 """Return history for the from_jid/to_jid couple""" | |
926 sat_session = session_iface.ISATSession(self.session) | |
927 profile = sat_session.profile | |
928 sat_jid = sat_session.jid | |
929 if not sat_jid: | |
930 raise exceptions.InternalError("session jid should be set") | |
931 if ( | |
932 jid.JID(from_jid).userhost() != sat_jid.userhost() | |
933 and jid.JID(to_jid).userhost() != sat_jid.userhost() | |
934 ): | |
935 log.error( | |
936 u"Trying to get history from a different jid (given (browser): {}, real " | |
937 u"(backend): {}), maybe a hack attempt ?".format( from_jid, sat_jid)) | |
938 return {} | |
939 d = self.asyncBridgeCall( | |
940 "historyGet", from_jid, to_jid, size, between, search, profile) | |
941 | |
942 def show(result_dbus): | |
943 result = [] | |
944 for line in result_dbus: | |
945 # XXX: we have to do this stupid thing because Python D-Bus use its own | |
946 # types instead of standard types and txJsonRPC doesn't accept | |
947 # D-Bus types, resulting in a empty query | |
948 uuid, timestamp, from_jid, to_jid, message, subject, mess_type, extra = ( | |
949 line | |
950 ) | |
951 result.append( | |
952 ( | |
953 unicode(uuid), | |
954 float(timestamp), | |
955 unicode(from_jid), | |
956 unicode(to_jid), | |
957 dict(message), | |
958 dict(subject), | |
959 unicode(mess_type), | |
960 dict(extra), | |
961 ) | |
962 ) | |
963 return result | |
964 | |
965 d.addCallback(show) | |
966 return d | |
967 | |
968 def jsonrpc_mucJoin(self, room_jid, nick): | |
969 """Join a Multi-User Chat room | |
970 | |
971 @param room_jid (unicode): room JID or empty string to generate a unique name | |
972 @param nick (unicode): user nick | |
973 """ | |
974 profile = session_iface.ISATSession(self.session).profile | |
975 d = self.asyncBridgeCall("joinMUC", room_jid, nick, {}, profile) | |
976 return d | |
977 | |
978 def jsonrpc_inviteMUC(self, contact_jid, room_jid): | |
979 """Invite a user to a Multi-User Chat room | |
980 | |
981 @param contact_jid (unicode): contact to invite | |
982 @param room_jid (unicode): room JID or empty string to generate a unique name | |
983 """ | |
984 profile = session_iface.ISATSession(self.session).profile | |
985 room_id = room_jid.split("@")[0] | |
986 service = room_jid.split("@")[1] | |
987 return self.sat_host.bridgeCall( | |
988 "inviteMUC", contact_jid, service, room_id, {}, profile | |
989 ) | |
990 | |
991 def jsonrpc_mucLeave(self, room_jid): | |
992 """Quit a Multi-User Chat room""" | |
993 profile = session_iface.ISATSession(self.session).profile | |
994 try: | |
995 room_jid = jid.JID(room_jid) | |
996 except: | |
997 log.warning("Invalid room jid") | |
998 return | |
999 return self.sat_host.bridgeCall("mucLeave", room_jid.userhost(), profile) | |
1000 | |
1001 def jsonrpc_mucGetRoomsJoined(self): | |
1002 """Return list of room already joined by user""" | |
1003 profile = session_iface.ISATSession(self.session).profile | |
1004 return self.sat_host.bridgeCall("mucGetRoomsJoined", profile) | |
1005 | |
1006 def jsonrpc_mucGetDefaultService(self): | |
1007 """@return: the default MUC""" | |
1008 d = self.asyncBridgeCall("mucGetDefaultService") | |
1009 return d | |
1010 | |
1011 def jsonrpc_launchTarotGame(self, other_players, room_jid=""): | |
1012 """Create a room, invite the other players and start a Tarot game. | |
1013 | |
1014 @param other_players (list[unicode]): JIDs of the players to play with | |
1015 @param room_jid (unicode): room JID or empty string to generate a unique name | |
1016 """ | |
1017 profile = session_iface.ISATSession(self.session).profile | |
1018 return self.sat_host.bridgeCall( | |
1019 "tarotGameLaunch", other_players, room_jid, profile | |
1020 ) | |
1021 | |
1022 def jsonrpc_getTarotCardsPaths(self): | |
1023 """Give the path of all the tarot cards""" | |
1024 _join = os.path.join | |
1025 _media_dir = _join(self.sat_host.media_dir, "") | |
1026 return map( | |
1027 lambda x: _join(C.MEDIA_DIR, x[len(_media_dir) :]), | |
1028 glob.glob(_join(_media_dir, C.CARDS_DIR, "*_*.png")), | |
1029 ) | |
1030 | |
1031 def jsonrpc_tarotGameReady(self, player, referee): | |
1032 """Tell to the server that we are ready to start the game""" | |
1033 profile = session_iface.ISATSession(self.session).profile | |
1034 return self.sat_host.bridgeCall("tarotGameReady", player, referee, profile) | |
1035 | |
1036 def jsonrpc_tarotGamePlayCards(self, player_nick, referee, cards): | |
1037 """Tell to the server the cards we want to put on the table""" | |
1038 profile = session_iface.ISATSession(self.session).profile | |
1039 return self.sat_host.bridgeCall( | |
1040 "tarotGamePlayCards", player_nick, referee, cards, profile | |
1041 ) | |
1042 | |
1043 def jsonrpc_launchRadioCollective(self, invited, room_jid=""): | |
1044 """Create a room, invite people, and start a radio collective. | |
1045 | |
1046 @param invited (list[unicode]): JIDs of the contacts to play with | |
1047 @param room_jid (unicode): room JID or empty string to generate a unique name | |
1048 """ | |
1049 profile = session_iface.ISATSession(self.session).profile | |
1050 return self.sat_host.bridgeCall("radiocolLaunch", invited, room_jid, profile) | |
1051 | |
1052 def jsonrpc_getEntitiesData(self, jids, keys): | |
1053 """Get cached data for several entities at once | |
1054 | |
1055 @param jids: list jids from who we wants data, or empty list for all jids in cache | |
1056 @param keys: name of data we want (list) | |
1057 @return: requested data""" | |
1058 if not C.ALLOWED_ENTITY_DATA.issuperset(keys): | |
1059 raise exceptions.PermissionError( | |
1060 "Trying to access unallowed data (hack attempt ?)" | |
1061 ) | |
1062 profile = session_iface.ISATSession(self.session).profile | |
1063 try: | |
1064 return self.sat_host.bridgeCall("getEntitiesData", jids, keys, profile) | |
1065 except Exception as e: | |
1066 raise failure.Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(e))) | |
1067 | |
1068 def jsonrpc_getEntityData(self, jid, keys): | |
1069 """Get cached data for an entity | |
1070 | |
1071 @param jid: jid of contact from who we want data | |
1072 @param keys: name of data we want (list) | |
1073 @return: requested data""" | |
1074 if not C.ALLOWED_ENTITY_DATA.issuperset(keys): | |
1075 raise exceptions.PermissionError( | |
1076 "Trying to access unallowed data (hack attempt ?)" | |
1077 ) | |
1078 profile = session_iface.ISATSession(self.session).profile | |
1079 try: | |
1080 return self.sat_host.bridgeCall("getEntityData", jid, keys, profile) | |
1081 except Exception as e: | |
1082 raise failure.Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(e))) | |
1083 | |
1084 def jsonrpc_getCard(self, jid_): | |
1085 """Get VCard for entiry | |
1086 @param jid_: jid of contact from who we want data | |
1087 @return: id to retrieve the profile""" | |
1088 profile = session_iface.ISATSession(self.session).profile | |
1089 return self.sat_host.bridgeCall("getCard", jid_, profile) | |
1090 | |
1091 @defer.inlineCallbacks | |
1092 def jsonrpc_avatarGet(self, entity, cache_only, hash_only): | |
1093 session_data = session_iface.ISATSession(self.session) | |
1094 profile = session_data.profile | |
1095 # profile_uuid = session_data.uuid | |
1096 avatar = yield self.asyncBridgeCall( | |
1097 "avatarGet", entity, cache_only, hash_only, profile | |
1098 ) | |
1099 if hash_only: | |
1100 defer.returnValue(avatar) | |
1101 else: | |
1102 filename = os.path.basename(avatar) | |
1103 avatar_url = os.path.join(session_data.cache_dir, filename) | |
1104 defer.returnValue(avatar_url) | |
1105 | |
1106 def jsonrpc_getAccountDialogUI(self): | |
1107 """Get the dialog for managing user account | |
1108 @return: XML string of the XMLUI""" | |
1109 profile = session_iface.ISATSession(self.session).profile | |
1110 return self.sat_host.bridgeCall("getAccountDialogUI", profile) | |
1111 | |
1112 def jsonrpc_getParamsUI(self): | |
1113 """Return the parameters XML for profile""" | |
1114 profile = session_iface.ISATSession(self.session).profile | |
1115 return self.asyncBridgeCall("getParamsUI", C.SECURITY_LIMIT, C.APP_NAME, profile) | |
1116 | |
1117 def jsonrpc_asyncGetParamA(self, param, category, attribute="value"): | |
1118 """Return the parameter value for profile""" | |
1119 profile = session_iface.ISATSession(self.session).profile | |
1120 if category == "Connection": | |
1121 # we need to manage the followings params here, else SECURITY_LIMIT would | |
1122 # block them | |
1123 if param == "JabberID": | |
1124 return self.asyncBridgeCall( | |
1125 "asyncGetParamA", param, category, attribute, profile_key=profile | |
1126 ) | |
1127 elif param == "autoconnect": | |
1128 return defer.succeed(C.BOOL_TRUE) | |
1129 d = self.asyncBridgeCall( | |
1130 "asyncGetParamA", | |
1131 param, | |
1132 category, | |
1133 attribute, | |
1134 C.SECURITY_LIMIT, | |
1135 profile_key=profile, | |
1136 ) | |
1137 return d | |
1138 | |
1139 def jsonrpc_setParam(self, name, value, category): | |
1140 profile = session_iface.ISATSession(self.session).profile | |
1141 return self.sat_host.bridgeCall( | |
1142 "setParam", name, value, category, C.SECURITY_LIMIT, profile | |
1143 ) | |
1144 | |
1145 def jsonrpc_launchAction(self, callback_id, data): | |
1146 # FIXME: any action can be launched, this can be a huge security issue if | |
1147 # callback_id can be guessed a security system with authorised | |
1148 # callback_id must be implemented, similar to the one for authorised params | |
1149 profile = session_iface.ISATSession(self.session).profile | |
1150 d = self.asyncBridgeCall("launchAction", callback_id, data, profile) | |
1151 return d | |
1152 | |
1153 def jsonrpc_chatStateComposing(self, to_jid_s): | |
1154 """Call the method to process a "composing" state. | |
1155 @param to_jid_s: contact the user is composing to | |
1156 """ | |
1157 profile = session_iface.ISATSession(self.session).profile | |
1158 return self.sat_host.bridgeCall("chatStateComposing", to_jid_s, profile) | |
1159 | |
1160 def jsonrpc_getNewAccountDomain(self): | |
1161 """@return: the domain for new account creation""" | |
1162 d = self.asyncBridgeCall("getNewAccountDomain") | |
1163 return d | |
1164 | |
1165 def jsonrpc_syntaxConvert( | |
1166 self, text, syntax_from=C.SYNTAX_XHTML, syntax_to=C.SYNTAX_CURRENT | |
1167 ): | |
1168 """ Convert a text between two syntaxes | |
1169 @param text: text to convert | |
1170 @param syntax_from: source syntax (e.g. "markdown") | |
1171 @param syntax_to: dest syntax (e.g.: "XHTML") | |
1172 @param safe: clean resulting XHTML to avoid malicious code if True (forced here) | |
1173 @return: converted text """ | |
1174 profile = session_iface.ISATSession(self.session).profile | |
1175 return self.sat_host.bridgeCall( | |
1176 "syntaxConvert", text, syntax_from, syntax_to, True, profile | |
1177 ) | |
1178 | |
1179 def jsonrpc_getLastResource(self, jid_s): | |
1180 """Get the last active resource of that contact.""" | |
1181 profile = session_iface.ISATSession(self.session).profile | |
1182 return self.sat_host.bridgeCall("getLastResource", jid_s, profile) | |
1183 | |
1184 def jsonrpc_getFeatures(self): | |
1185 """Return the available features in the backend for profile""" | |
1186 profile = session_iface.ISATSession(self.session).profile | |
1187 return self.sat_host.bridgeCall("getFeatures", profile) | |
1188 | |
1189 def jsonrpc_skipOTR(self): | |
1190 """Tell the backend to leave OTR handling to Libervia.""" | |
1191 profile = session_iface.ISATSession(self.session).profile | |
1192 return self.sat_host.bridgeCall("skipOTR", profile) | |
1193 | |
1194 def jsonrpc_namespacesGet(self): | |
1195 return self.sat_host.bridgeCall("namespacesGet") | |
1196 | |
1197 | |
1198 class WaitingRequests(dict): | 565 class WaitingRequests(dict): |
1199 def setRequest(self, request, profile, register_with_ext_jid=False): | 566 def setRequest(self, request, profile, register_with_ext_jid=False): |
1200 """Add the given profile to the waiting list. | 567 """Add the given profile to the waiting list. |
1201 | 568 |
1202 @param request (server.Request): the connection request | 569 @param request (server.Request): the connection request |
1235 @return: bool or None | 602 @return: bool or None |
1236 """ | 603 """ |
1237 return self[profile][2] if profile in self else None | 604 return self[profile][2] if profile in self else None |
1238 | 605 |
1239 | 606 |
1240 class Register(JSONRPCMethodManager): | |
1241 """This class manage the registration procedure with SàT | |
1242 It provide an api for the browser, check password and setup the web server""" | |
1243 | |
1244 def __init__(self, sat_host): | |
1245 JSONRPCMethodManager.__init__(self, sat_host) | |
1246 self.profiles_waiting = {} | |
1247 self.request = None | |
1248 | |
1249 def render(self, request): | |
1250 """ | |
1251 Render method with some hacks: | |
1252 - if login is requested, try to login with form data | |
1253 - except login, every method is jsonrpc | |
1254 - user doesn't need to be authentified for explicitely listed methods, | |
1255 but must be for all others | |
1256 """ | |
1257 if request.postpath == ["login"]: | |
1258 return self.loginOrRegister(request) | |
1259 _session = request.getSession() | |
1260 parsed = jsonrpclib.loads(request.content.read()) | |
1261 method = parsed.get("method") # pylint: disable=E1103 | |
1262 if method not in ["getSessionMetadata", "registerParams", "menusGet"]: | |
1263 # if we don't call these methods, we need to be identified | |
1264 profile = session_iface.ISATSession(_session).profile | |
1265 if not profile: | |
1266 # user is not identified, we return a jsonrpc fault | |
1267 fault = jsonrpclib.Fault( | |
1268 C.ERRNUM_LIBERVIA, C.NOT_ALLOWED | |
1269 ) # FIXME: define some standard error codes for libervia | |
1270 return jsonrpc.JSONRPC._cbRender( | |
1271 self, fault, request, parsed.get("id"), parsed.get("jsonrpc") | |
1272 ) # pylint: disable=E1103 | |
1273 self.request = request | |
1274 return jsonrpc.JSONRPC.render(self, request) | |
1275 | |
1276 def loginOrRegister(self, request): | |
1277 """This method is called with the POST information from the registering form. | |
1278 | |
1279 @param request: request of the register form | |
1280 @return: a constant indicating the state: | |
1281 - C.BAD_REQUEST: something is wrong in the request (bad arguments) | |
1282 - a return value from self._loginAccount or self._registerNewAccount | |
1283 """ | |
1284 try: | |
1285 submit_type = request.args["submit_type"][0] | |
1286 except KeyError: | |
1287 return C.BAD_REQUEST | |
1288 | |
1289 if submit_type == "register": | |
1290 self._registerNewAccount(request) | |
1291 return server.NOT_DONE_YET | |
1292 elif submit_type == "login": | |
1293 self._loginAccount(request) | |
1294 return server.NOT_DONE_YET | |
1295 return Exception("Unknown submit type") | |
1296 | |
1297 @defer.inlineCallbacks | |
1298 def _registerNewAccount(self, request): | |
1299 try: | |
1300 login = request.args["register_login"][0] | |
1301 password = request.args["register_password"][0] | |
1302 email = request.args["email"][0] | |
1303 except KeyError: | |
1304 request.write(C.BAD_REQUEST) | |
1305 request.finish() | |
1306 return | |
1307 status = yield self.sat_host.registerNewAccount(request, login, password, email) | |
1308 request.write(status) | |
1309 request.finish() | |
1310 | |
1311 @defer.inlineCallbacks | |
1312 def _loginAccount(self, request): | |
1313 """Try to authenticate the user with the request information. | |
1314 | |
1315 will write to request a constant indicating the state: | |
1316 - C.PROFILE_LOGGED: profile is connected | |
1317 - C.PROFILE_LOGGED_EXT_JID: profile is connected and an external jid has | |
1318 been used | |
1319 - C.SESSION_ACTIVE: session was already active | |
1320 - C.BAD_REQUEST: something is wrong in the request (bad arguments) | |
1321 - C.PROFILE_AUTH_ERROR: either the profile (login) or the profile password | |
1322 is wrong | |
1323 - C.XMPP_AUTH_ERROR: the profile is authenticated but the XMPP password | |
1324 is wrong | |
1325 - C.ALREADY_WAITING: a request has already been submitted for this profile, | |
1326 C.PROFILE_LOGGED_EXT_JID) | |
1327 - C.NOT_CONNECTED: connection has not been established | |
1328 the request will then be finished | |
1329 @param request: request of the register form | |
1330 """ | |
1331 try: | |
1332 login = request.args["login"][0] | |
1333 password = request.args["login_password"][0] | |
1334 except KeyError: | |
1335 request.write(C.BAD_REQUEST) | |
1336 request.finish() | |
1337 return | |
1338 | |
1339 assert login | |
1340 | |
1341 try: | |
1342 status = yield self.sat_host.connect(request, login, password) | |
1343 except ( | |
1344 exceptions.DataError, | |
1345 exceptions.ProfileUnknownError, | |
1346 exceptions.PermissionError, | |
1347 ): | |
1348 request.write(C.PROFILE_AUTH_ERROR) | |
1349 request.finish() | |
1350 return | |
1351 except exceptions.NotReady: | |
1352 request.write(C.ALREADY_WAITING) | |
1353 request.finish() | |
1354 return | |
1355 except exceptions.TimeOutError: | |
1356 request.write(C.NO_REPLY) | |
1357 request.finish() | |
1358 return | |
1359 except exceptions.InternalError as e: | |
1360 request.write(e.message) | |
1361 request.finish() | |
1362 return | |
1363 except exceptions.ConflictError: | |
1364 request.write(C.SESSION_ACTIVE) | |
1365 request.finish() | |
1366 return | |
1367 except ValueError as e: | |
1368 if e.message in (C.PROFILE_AUTH_ERROR, C.XMPP_AUTH_ERROR): | |
1369 request.write(e.message) | |
1370 request.finish() | |
1371 return | |
1372 else: | |
1373 raise e | |
1374 | |
1375 assert status | |
1376 request.write(status) | |
1377 request.finish() | |
1378 | |
1379 def jsonrpc_isConnected(self): | |
1380 _session = self.request.getSession() | |
1381 profile = session_iface.ISATSession(_session).profile | |
1382 return self.sat_host.bridgeCall("isConnected", profile) | |
1383 | |
1384 def jsonrpc_connect(self): | |
1385 _session = self.request.getSession() | |
1386 profile = session_iface.ISATSession(_session).profile | |
1387 if self.waiting_profiles.getRequest(profile): | |
1388 raise jsonrpclib.Fault( | |
1389 1, C.ALREADY_WAITING | |
1390 ) # FIXME: define some standard error codes for libervia | |
1391 self.waiting_profiles.setRequest(self.request, profile) | |
1392 self.sat_host.bridgeCall("connect", profile) | |
1393 return server.NOT_DONE_YET | |
1394 | |
1395 def jsonrpc_getSessionMetadata(self): | |
1396 """Return metadata useful on session start | |
1397 | |
1398 @return (dict): metadata which can have the following keys: | |
1399 "plugged" (bool): True if a profile is already plugged | |
1400 "warning" (unicode): a security warning message if plugged is False and if | |
1401 it make sense. | |
1402 This key may not be present. | |
1403 "allow_registration" (bool): True if registration is allowed | |
1404 this key is only present if profile is unplugged | |
1405 @return: a couple (registered, message) with: | |
1406 - registered: | |
1407 - message: | |
1408 """ | |
1409 metadata = {} | |
1410 _session = self.request.getSession() | |
1411 profile = session_iface.ISATSession(_session).profile | |
1412 if profile: | |
1413 metadata["plugged"] = True | |
1414 else: | |
1415 metadata["plugged"] = False | |
1416 metadata["warning"] = self._getSecurityWarning() | |
1417 metadata["allow_registration"] = self.sat_host.options["allow_registration"] | |
1418 return metadata | |
1419 | |
1420 def jsonrpc_registerParams(self): | |
1421 """Register the frontend specific parameters""" | |
1422 # params = """<params><individual>...</category></individual>""" | |
1423 # self.sat_host.bridge.paramsRegisterApp(params, C.SECURITY_LIMIT, C.APP_NAME) | |
1424 | |
1425 def jsonrpc_menusGet(self): | |
1426 """Return the parameters XML for profile""" | |
1427 # XXX: we put this method in Register because we get menus before being logged | |
1428 return self.sat_host.bridgeCall("menusGet", "", C.SECURITY_LIMIT) | |
1429 | |
1430 def _getSecurityWarning(self): | |
1431 """@return: a security warning message, or None if the connection is secure""" | |
1432 if ( | |
1433 self.request.URLPath().scheme == "https" | |
1434 or not self.sat_host.options["security_warning"] | |
1435 ): | |
1436 return None | |
1437 text = ( | |
1438 "<p>" | |
1439 + D_("You are about to connect to an unsecure service.") | |
1440 + "</p><p> </p><p>" | |
1441 ) | |
1442 | |
1443 if self.sat_host.options["connection_type"] == "both": | |
1444 new_port = ( | |
1445 (":%s" % self.sat_host.options["port_https_ext"]) | |
1446 if self.sat_host.options["port_https_ext"] != HTTPS_PORT | |
1447 else "" | |
1448 ) | |
1449 url = "https://%s" % self.request.URLPath().netloc.replace( | |
1450 ":%s" % self.sat_host.options["port"], new_port | |
1451 ) | |
1452 text += D_( | |
1453 "Please read our %(faq_prefix)ssecurity notice%(faq_suffix)s regarding HTTPS" | |
1454 ) % { | |
1455 "faq_prefix": '<a href="http://salut-a-toi.org/faq.html#https" target="#">', | |
1456 "faq_suffix": "</a>", | |
1457 } | |
1458 text += "</p><p>" + D_("and use the secure version of this website:") | |
1459 text += '</p><p> </p><p align="center"><a href="%(url)s">%(url)s</a>' % { | |
1460 "url": url | |
1461 } | |
1462 else: | |
1463 text += D_("You should ask your administrator to turn on HTTPS.") | |
1464 | |
1465 return text + "</p><p> </p>" | |
1466 | |
1467 | |
1468 class SignalHandler(jsonrpc.JSONRPC): | |
1469 def __init__(self, sat_host): | |
1470 web_resource.Resource.__init__(self) | |
1471 self.register = None | |
1472 self.sat_host = sat_host | |
1473 self._last_service_prof_disconnect = time.time() | |
1474 self.signalDeferred = {} # dict of deferred (key: profile, value: Deferred) | |
1475 # which manages the long polling HTTP request with signals | |
1476 self.queue = {} | |
1477 | |
1478 def plugRegister(self, register): | |
1479 self.register = register | |
1480 | |
1481 def jsonrpc_getSignals(self): | |
1482 """Keep the connection alive until a signal is received, then send it | |
1483 @return: (signal, *signal_args)""" | |
1484 _session = self.request.getSession() | |
1485 profile = session_iface.ISATSession(_session).profile | |
1486 if profile in self.queue: # if we have signals to send in queue | |
1487 if self.queue[profile]: | |
1488 return self.queue[profile].pop(0) | |
1489 else: | |
1490 # the queue is empty, we delete the profile from queue | |
1491 del self.queue[profile] | |
1492 _session.lock() # we don't want the session to expire as long as this | |
1493 # connection is active | |
1494 | |
1495 def unlock(signal, profile): | |
1496 _session.unlock() | |
1497 try: | |
1498 source_defer = self.signalDeferred[profile] | |
1499 if source_defer.called and source_defer.result[0] == "disconnected": | |
1500 log.info(u"[%s] disconnected" % (profile,)) | |
1501 try: | |
1502 _session.expire() | |
1503 except KeyError: | |
1504 # FIXME: happen if session is ended using login page | |
1505 # when pyjamas page is also launched | |
1506 log.warning(u"session is already expired") | |
1507 except IndexError: | |
1508 log.error("Deferred result should be a tuple with fonction name first") | |
1509 | |
1510 self.signalDeferred[profile] = defer.Deferred() | |
1511 self.request.notifyFinish().addBoth(unlock, profile) | |
1512 return self.signalDeferred[profile] | |
1513 | |
1514 def getGenericCb(self, function_name): | |
1515 """Return a generic function which send all params to signalDeferred.callback | |
1516 function must have profile as last argument""" | |
1517 | |
1518 def genericCb(*args): | |
1519 profile = args[-1] | |
1520 if not profile in self.sat_host.prof_connected: | |
1521 return | |
1522 signal_data = (function_name, args[:-1]) | |
1523 try: | |
1524 signal_callback = self.signalDeferred[profile].callback | |
1525 except KeyError: | |
1526 self.queue.setdefault(profile, []).append(signal_data) | |
1527 else: | |
1528 signal_callback(signal_data) | |
1529 del self.signalDeferred[profile] | |
1530 | |
1531 return genericCb | |
1532 | |
1533 def actionNewHandler(self, action_data, action_id, security_limit, profile): | |
1534 """actionNew handler | |
1535 | |
1536 XXX: We need need a dedicated handler has actionNew use a security_limit | |
1537 which must be managed | |
1538 @param action_data(dict): see bridge documentation | |
1539 @param action_id(unicode): identitifer of the action | |
1540 @param security_limit(int): %(doc_security_limit)s | |
1541 @param profile(unicode): %(doc_profile)s | |
1542 """ | |
1543 if not profile in self.sat_host.prof_connected: | |
1544 return | |
1545 # FIXME: manage security limit in a dedicated method | |
1546 # raise an exception if it's not OK | |
1547 # and read value in sat.conf | |
1548 if security_limit >= C.SECURITY_LIMIT: | |
1549 log.debug( | |
1550 u"Ignoring action {action_id}, blocked by security limit".format( | |
1551 action_id=action_id | |
1552 ) | |
1553 ) | |
1554 return | |
1555 signal_data = ("actionNew", (action_data, action_id, security_limit)) | |
1556 try: | |
1557 signal_callback = self.signalDeferred[profile].callback | |
1558 except KeyError: | |
1559 self.queue.setdefault(profile, []).append(signal_data) | |
1560 else: | |
1561 signal_callback(signal_data) | |
1562 del self.signalDeferred[profile] | |
1563 | |
1564 def connected(self, profile, jid_s): | |
1565 """Connection is done. | |
1566 | |
1567 @param profile (unicode): %(doc_profile)s | |
1568 @param jid_s (unicode): the JID that we were assigned by the server, as the | |
1569 resource might differ from the JID we asked for. | |
1570 """ | |
1571 # FIXME: _logged should not be called from here, check this code | |
1572 # FIXME: check if needed to connect with external jid | |
1573 # jid_s is handled in QuickApp.connectionHandler already | |
1574 # assert self.register # register must be plugged | |
1575 # request = self.sat_host.waiting_profiles.getRequest(profile) | |
1576 # if request: | |
1577 # self.sat_host._logged(profile, request) | |
1578 | |
1579 def disconnected(self, profile): | |
1580 if profile == C.SERVICE_PROFILE: | |
1581 # if service profile has been disconnected, we try to reconnect it | |
1582 # if we can't we show error message | |
1583 # and if we have 2 disconnection in a short time, we don't try to reconnect | |
1584 # and display an error message | |
1585 disconnect_delta = time.time() - self._last_service_prof_disconnect | |
1586 if disconnect_delta < 15: | |
1587 log.error( | |
1588 _(u"Service profile disconnected twice in a short time, please " | |
1589 u"check connection")) | |
1590 else: | |
1591 log.info( | |
1592 _(u"Service profile has been disconnected, but we need it! " | |
1593 u"Reconnecting it...")) | |
1594 d = self.sat_host.bridgeCall( | |
1595 "connect", profile, self.sat_host.options["passphrase"], {} | |
1596 ) | |
1597 d.addErrback( | |
1598 lambda failure_: log.error(_( | |
1599 u"Can't reconnect service profile, please check connection: " | |
1600 u"{reason}").format(reason=failure_))) | |
1601 self._last_service_prof_disconnect = time.time() | |
1602 return | |
1603 | |
1604 if not profile in self.sat_host.prof_connected: | |
1605 log.info(_(u"'disconnected' signal received for a not connected profile " | |
1606 u"({profile})").format(profile=profile)) | |
1607 return | |
1608 self.sat_host.prof_connected.remove(profile) | |
1609 if profile in self.signalDeferred: | |
1610 self.signalDeferred[profile].callback(("disconnected",)) | |
1611 del self.signalDeferred[profile] | |
1612 else: | |
1613 if profile not in self.queue: | |
1614 self.queue[profile] = [] | |
1615 self.queue[profile].append(("disconnected",)) | |
1616 | |
1617 def render(self, request): | |
1618 """ | |
1619 Render method wich reject access if user is not identified | |
1620 """ | |
1621 _session = request.getSession() | |
1622 parsed = jsonrpclib.loads(request.content.read()) | |
1623 profile = session_iface.ISATSession(_session).profile | |
1624 if not profile: | |
1625 # FIXME: this method should not use _cbRender | |
1626 # but all txJsonRPC code will be removed in 0.8 in favor of webRTC | |
1627 # and it is currently used only with Libervia legacy app, | |
1628 # so we do a is_jsonp workaround for now | |
1629 self.is_jsonp = False | |
1630 # user is not identified, we return a jsonrpc fault | |
1631 fault = jsonrpclib.Fault( | |
1632 C.ERRNUM_LIBERVIA, C.NOT_ALLOWED | |
1633 ) # FIXME: define some standard error codes for libervia | |
1634 return jsonrpc.JSONRPC._cbRender( | |
1635 self, fault, request, parsed.get("id"), parsed.get("jsonrpc") | |
1636 ) | |
1637 self.request = request | |
1638 return jsonrpc.JSONRPC.render(self, request) | |
1639 | |
1640 | |
1641 class UploadManager(web_resource.Resource): | |
1642 """This class manage the upload of a file | |
1643 It redirect the stream to SàT core backend""" | |
1644 | |
1645 isLeaf = True | |
1646 NAME = "path" # name use by the FileUpload | |
1647 | |
1648 def __init__(self, sat_host): | |
1649 self.sat_host = sat_host | |
1650 self.upload_dir = tempfile.mkdtemp() | |
1651 self.sat_host.addCleanup(shutil.rmtree, self.upload_dir) | |
1652 | |
1653 def getTmpDir(self): | |
1654 return self.upload_dir | |
1655 | |
1656 def _getFileName(self, request): | |
1657 """Generate unique filename for a file""" | |
1658 raise NotImplementedError | |
1659 | |
1660 def _fileWritten(self, request, filepath): | |
1661 """Called once the file is actually written on disk | |
1662 @param request: HTTP request object | |
1663 @param filepath: full filepath on the server | |
1664 @return: a tuple with the name of the async bridge method | |
1665 to be called followed by its arguments. | |
1666 """ | |
1667 raise NotImplementedError | |
1668 | |
1669 def render(self, request): | |
1670 """ | |
1671 Render method with some hacks: | |
1672 - if login is requested, try to login with form data | |
1673 - except login, every method is jsonrpc | |
1674 - user doesn't need to be authentified for getSessionMetadata, but must be | |
1675 for all other methods | |
1676 """ | |
1677 filename = self._getFileName(request) | |
1678 filepath = os.path.join(self.upload_dir, filename) | |
1679 # FIXME: the uploaded file is fully loaded in memory at form parsing time so far | |
1680 # (see twisted.web.http.Request.requestReceived). A custom requestReceived | |
1681 # should be written in the futur. In addition, it is not yet possible to | |
1682 # get progression informations (see | |
1683 # http://twistedmatrix.com/trac/ticket/288) | |
1684 | |
1685 with open(filepath, "w") as f: | |
1686 f.write(request.args[self.NAME][0]) | |
1687 | |
1688 def finish(d): | |
1689 error = isinstance(d, Exception) or isinstance(d, failure.Failure) | |
1690 request.write(C.UPLOAD_KO if error else C.UPLOAD_OK) | |
1691 # TODO: would be great to re-use the original Exception class and message | |
1692 # but it is lost in the middle of the backtrace and encapsulated within | |
1693 # a DBusException instance --> extract the data from the backtrace? | |
1694 request.finish() | |
1695 | |
1696 d = JSONRPCMethodManager(self.sat_host).asyncBridgeCall( | |
1697 *self._fileWritten(request, filepath) | |
1698 ) | |
1699 d.addCallbacks(lambda d: finish(d), lambda failure: finish(failure)) | |
1700 return server.NOT_DONE_YET | |
1701 | |
1702 | |
1703 class UploadManagerRadioCol(UploadManager): | |
1704 NAME = "song" | |
1705 | |
1706 def _getFileName(self, request): | |
1707 extension = os.path.splitext(request.args["filename"][0])[1] | |
1708 return "%s%s" % ( | |
1709 str(uuid.uuid4()), | |
1710 extension, | |
1711 ) # XXX: chromium doesn't seem to play song without the .ogg extension, even | |
1712 # with audio/ogg mime-type | |
1713 | |
1714 def _fileWritten(self, request, filepath): | |
1715 """Called once the file is actually written on disk | |
1716 @param request: HTTP request object | |
1717 @param filepath: full filepath on the server | |
1718 @return: a tuple with the name of the async bridge method | |
1719 to be called followed by its arguments. | |
1720 """ | |
1721 profile = session_iface.ISATSession(request.getSession()).profile | |
1722 return ("radiocolSongAdded", request.args["referee"][0], filepath, profile) | |
1723 | |
1724 | |
1725 class UploadManagerAvatar(UploadManager): | |
1726 NAME = "avatar_path" | |
1727 | |
1728 def _getFileName(self, request): | |
1729 return str(uuid.uuid4()) | |
1730 | |
1731 def _fileWritten(self, request, filepath): | |
1732 """Called once the file is actually written on disk | |
1733 @param request: HTTP request object | |
1734 @param filepath: full filepath on the server | |
1735 @return: a tuple with the name of the async bridge method | |
1736 to be called followed by its arguments. | |
1737 """ | |
1738 profile = session_iface.ISATSession(request.getSession()).profile | |
1739 return ("setAvatar", filepath, profile) | |
1740 | |
1741 | |
1742 class Libervia(service.Service): | 607 class Libervia(service.Service): |
1743 debug = defer.Deferred.debug # True if twistd/Libervia is launched in debug mode | 608 debug = defer.Deferred.debug # True if twistd/Libervia is launched in debug mode |
1744 | 609 |
1745 def __init__(self, options): | 610 def __init__(self, options): |
1746 self.options = options | 611 self.options = options |
1751 | 616 |
1752 if self.options["base_url_ext"]: | 617 if self.options["base_url_ext"]: |
1753 self.base_url_ext = self.options.pop("base_url_ext") | 618 self.base_url_ext = self.options.pop("base_url_ext") |
1754 if self.base_url_ext[-1] != "/": | 619 if self.base_url_ext[-1] != "/": |
1755 self.base_url_ext += "/" | 620 self.base_url_ext += "/" |
1756 self.base_url_ext_data = urlparse.urlsplit(self.base_url_ext) | 621 self.base_url_ext_data = urllib.parse.urlsplit(self.base_url_ext) |
1757 else: | 622 else: |
1758 self.base_url_ext = None | 623 self.base_url_ext = None |
1759 # we split empty string anyway so we can do things like | 624 # we split empty string anyway so we can do things like |
1760 # scheme = self.base_url_ext_data.scheme or 'https' | 625 # scheme = self.base_url_ext_data.scheme or 'https' |
1761 self.base_url_ext_data = urlparse.urlsplit("") | 626 self.base_url_ext_data = urllib.parse.urlsplit("") |
1762 | 627 |
1763 if not self.options["port_https_ext"]: | 628 if not self.options["port_https_ext"]: |
1764 self.options["port_https_ext"] = self.options["port_https"] | 629 self.options["port_https_ext"] = self.options["port_https"] |
1765 if self.options["data_dir"] == DATA_DIR_DEFAULT: | |
1766 coerceDataDir( | |
1767 self.options["data_dir"] | |
1768 ) # this is not done when using the default value | |
1769 | |
1770 self.html_dir = os.path.join(self.options["data_dir"], C.HTML_DIR) | |
1771 self.themes_dir = os.path.join(self.options["data_dir"], C.THEMES_DIR) | |
1772 | 630 |
1773 self._cleanup = [] | 631 self._cleanup = [] |
1774 | 632 |
1775 self.signal_handler = SignalHandler(self) | |
1776 self.sessions = {} # key = session value = user | 633 self.sessions = {} # key = session value = user |
1777 self.prof_connected = set() # Profiles connected | 634 self.prof_connected = set() # Profiles connected |
1778 self.ns_map = {} # map of short name to namespaces | 635 self.ns_map = {} # map of short name to namespaces |
1779 | 636 |
1780 ## bridge ## | 637 ## bridge ## |
1818 | 675 |
1819 """ | 676 """ |
1820 section = site_root_res.site_name.lower().strip() | 677 section = site_root_res.site_name.lower().strip() |
1821 value = config.getConfig(self.main_conf, section, key, default=default) | 678 value = config.getConfig(self.main_conf, section, key, default=default) |
1822 if value_type is not None: | 679 if value_type is not None: |
1823 if value_type == u'path': | 680 if value_type == 'path': |
1824 v_filter = lambda v: os.path.abspath(os.path.expanduser(v)) | 681 v_filter = lambda v: os.path.abspath(os.path.expanduser(v)) |
1825 else: | 682 else: |
1826 raise ValueError(u"unknown value type {value_type}".format( | 683 raise ValueError("unknown value type {value_type}".format( |
1827 value_type = value_type)) | 684 value_type = value_type)) |
1828 if isinstance(value, list): | 685 if isinstance(value, list): |
1829 value = [v_filter(v) for v in value] | 686 value = [v_filter(v) for v in value] |
1830 elif isinstance(value, dict): | 687 elif isinstance(value, dict): |
1831 value = {k:v_filter(v) for k,v in value.items()} | 688 value = {k:v_filter(v) for k,v in list(value.items())} |
1832 elif value is not None: | 689 elif value is not None: |
1833 value = v_filter(v) | 690 value = v_filter(v) |
1834 return value | 691 return value |
1835 | 692 |
1836 def _namespacesGetCb(self, ns_map): | 693 def _namespacesGetCb(self, ns_map): |
1837 self.ns_map = ns_map | 694 self.ns_map = ns_map |
1838 | 695 |
1839 def _namespacesGetEb(self, failure_): | 696 def _namespacesGetEb(self, failure_): |
1840 log.error(_(u"Can't get namespaces map: {msg}").format(msg=failure_)) | 697 log.error(_("Can't get namespaces map: {msg}").format(msg=failure_)) |
1841 | 698 |
1842 @template.contextfilter | 699 @template.contextfilter |
1843 def _front_url_filter(self, ctx, relative_url): | 700 def _front_url_filter(self, ctx, relative_url): |
1844 template_data = ctx[u'template_data'] | 701 template_data = ctx['template_data'] |
1845 return os.path.join(u'/', C.TPL_RESOURCE, template_data.site or u'sat', | 702 return os.path.join('/', C.TPL_RESOURCE, template_data.site or 'sat', |
1846 C.TEMPLATE_TPL_DIR, template_data.theme, relative_url) | 703 C.TEMPLATE_TPL_DIR, template_data.theme, relative_url) |
1847 | 704 |
1848 def _moveFirstLevelToDict(self, options, key, keys_to_keep): | 705 def _moveFirstLevelToDict(self, options, key, keys_to_keep): |
1849 """Read a config option and put value at first level into u'' dict | 706 """Read a config option and put value at first level into u'' dict |
1850 | 707 |
1858 try: | 715 try: |
1859 conf = options[key] | 716 conf = options[key] |
1860 except KeyError: | 717 except KeyError: |
1861 return | 718 return |
1862 if not isinstance(conf, dict): | 719 if not isinstance(conf, dict): |
1863 options[key] = {u'': conf} | 720 options[key] = {'': conf} |
1864 return | 721 return |
1865 default_dict = conf.get(u'', {}) | 722 default_dict = conf.get('', {}) |
1866 to_delete = [] | 723 to_delete = [] |
1867 for key, value in conf.iteritems(): | 724 for key, value in conf.items(): |
1868 if key not in keys_to_keep: | 725 if key not in keys_to_keep: |
1869 default_dict[key] = value | 726 default_dict[key] = value |
1870 to_delete.append(key) | 727 to_delete.append(key) |
1871 for key in to_delete: | 728 for key in to_delete: |
1872 del conf[key] | 729 del conf[key] |
1873 if default_dict: | 730 if default_dict: |
1874 conf[u''] = default_dict | 731 conf[''] = default_dict |
1875 | 732 |
1876 @defer.inlineCallbacks | 733 @defer.inlineCallbacks |
1877 def backendReady(self, __): | 734 def backendReady(self, __): |
1878 if self.options[u'dev_mode']: | 735 if self.options['dev_mode']: |
1879 log.info(_(u"Developer mode activated")) | 736 log.info(_("Developer mode activated")) |
1880 self.media_dir = self.bridge.getConfig("", "media_dir") | 737 self.media_dir = self.bridge.getConfig("", "media_dir") |
1881 self.local_dir = self.bridge.getConfig("", "local_dir") | 738 self.local_dir = self.bridge.getConfig("", "local_dir") |
1882 self.cache_root_dir = os.path.join(self.local_dir, C.CACHE_DIR) | 739 self.cache_root_dir = os.path.join(self.local_dir, C.CACHE_DIR) |
1883 self.renderer = template.Renderer(self, self._front_url_filter) | 740 self.renderer = template.Renderer(self, self._front_url_filter) |
1884 sites_names = self.renderer.sites_paths.keys() | 741 sites_names = list(self.renderer.sites_paths.keys()) |
1885 | 742 |
1886 self._moveFirstLevelToDict(self.options, "url_redirections_dict", sites_names) | 743 self._moveFirstLevelToDict(self.options, "url_redirections_dict", sites_names) |
1887 self._moveFirstLevelToDict(self.options, "menu_json", sites_names) | 744 self._moveFirstLevelToDict(self.options, "menu_json", sites_names) |
1888 if not u'' in self.options["menu_json"]: | 745 if not '' in self.options["menu_json"]: |
1889 self.options["menu_json"][u''] = C.DEFAULT_MENU | 746 self.options["menu_json"][''] = C.DEFAULT_MENU |
1890 | 747 |
1891 # we create virtual hosts and import Libervia pages into them | 748 # we create virtual hosts and import Libervia pages into them |
1892 self.vhost_root = vhost.NameVirtualHost() | 749 self.vhost_root = vhost.NameVirtualHost() |
1893 default_site_path = os.path.abspath(os.path.dirname(libervia.__file__)) | 750 default_site_path = os.path.abspath(os.path.dirname(libervia.__file__)) |
1894 # self.sat_root is official Libervia site | 751 # self.sat_root is official Libervia site |
752 root_path = os.path.join(default_site_path, C.TEMPLATE_STATIC_DIR) | |
1895 self.sat_root = default_root = LiberviaRootResource( | 753 self.sat_root = default_root = LiberviaRootResource( |
1896 host=self, host_name=u'', site_name=u'', site_path=default_site_path, | 754 host=self, host_name='', site_name='', site_path=default_site_path, |
1897 path=self.html_dir) | 755 path=root_path) |
1898 if self.options['dev_mode']: | 756 if self.options['dev_mode']: |
1899 self.files_watcher.watchDir( | 757 self.files_watcher.watchDir( |
1900 default_site_path, auto_add=True, recursive=True, | 758 default_site_path, auto_add=True, recursive=True, |
1901 callback=LiberviaPage.onFileChange, site_root=self.sat_root, | 759 callback=LiberviaPage.onFileChange, site_root=self.sat_root, |
1902 site_path=default_site_path) | 760 site_path=default_site_path) |
1904 yield tasks_manager.runTasks() | 762 yield tasks_manager.runTasks() |
1905 LiberviaPage.importPages(self, self.sat_root) | 763 LiberviaPage.importPages(self, self.sat_root) |
1906 # FIXME: handle _setMenu in a more generic way, taking care of external sites | 764 # FIXME: handle _setMenu in a more generic way, taking care of external sites |
1907 self.sat_root._setMenu(self.options["menu_json"]) | 765 self.sat_root._setMenu(self.options["menu_json"]) |
1908 self.vhost_root.default = default_root | 766 self.vhost_root.default = default_root |
1909 existing_vhosts = {u'': default_root} | 767 existing_vhosts = {b'': default_root} |
1910 | 768 |
1911 for host_name, site_name in self.options["vhosts_dict"].iteritems(): | 769 for host_name, site_name in self.options["vhosts_dict"].items(): |
770 encoded_site_name = site_name.encode('utf-8') | |
1912 try: | 771 try: |
1913 site_path = self.renderer.sites_paths[site_name] | 772 site_path = self.renderer.sites_paths[site_name] |
1914 except KeyError: | 773 except KeyError: |
1915 log.warning(_( | 774 log.warning(_( |
1916 u"host {host_name} link to non existing site {site_name}, ignoring " | 775 "host {host_name} link to non existing site {site_name}, ignoring " |
1917 u"it").format(host_name=host_name, site_name=site_name)) | 776 "it").format(host_name=host_name, site_name=site_name)) |
1918 continue | 777 continue |
1919 if site_name in existing_vhosts: | 778 if encoded_site_name in existing_vhosts: |
1920 # we have an alias host, we re-use existing resource | 779 # we have an alias host, we re-use existing resource |
1921 res = existing_vhosts[site_name] | 780 res = existing_vhosts[encoded_site_name] |
1922 else: | 781 else: |
1923 # for root path we first check if there is a global static dir | 782 # for root path we first check if there is a global static dir |
1924 # if not, we use default template's static dic | 783 # if not, we use default template's static dic |
1925 root_path = os.path.join(site_path, C.TEMPLATE_STATIC_DIR) | 784 root_path = os.path.join(site_path, C.TEMPLATE_STATIC_DIR) |
1926 if not os.path.isdir(root_path): | 785 if not os.path.isdir(root_path): |
1932 host_name=host_name, | 791 host_name=host_name, |
1933 site_name=site_name, | 792 site_name=site_name, |
1934 site_path=site_path, | 793 site_path=site_path, |
1935 path=root_path) | 794 path=root_path) |
1936 | 795 |
1937 existing_vhosts[site_name] = res | 796 existing_vhosts[encoded_site_name] = res |
1938 | 797 |
1939 if self.options['dev_mode']: | 798 if self.options['dev_mode']: |
1940 self.files_watcher.watchDir( | 799 self.files_watcher.watchDir( |
1941 site_path, auto_add=True, recursive=True, | 800 site_path, auto_add=True, recursive=True, |
1942 callback=LiberviaPage.onFileChange, site_root=res, | 801 callback=LiberviaPage.onFileChange, site_root=res, |
1958 res._setMenu(self.options["menu_json"]) | 817 res._setMenu(self.options["menu_json"]) |
1959 | 818 |
1960 self.vhost_root.addHost(host_name.encode('utf-8'), res) | 819 self.vhost_root.addHost(host_name.encode('utf-8'), res) |
1961 | 820 |
1962 templates_res = web_resource.Resource() | 821 templates_res = web_resource.Resource() |
1963 self.putChildAll(C.TPL_RESOURCE, templates_res) | 822 self.putChildAll(C.TPL_RESOURCE.encode('utf-8'), templates_res) |
1964 for site_name, site_path in self.renderer.sites_paths.iteritems(): | 823 for site_name, site_path in self.renderer.sites_paths.items(): |
1965 templates_res.putChild(site_name or u'sat', ProtectedFile(site_path)) | 824 templates_res.putChild(site_name.encode('utf-8') or b'sat', |
1966 | 825 ProtectedFile(site_path)) |
1967 _register = Register(self) | 826 |
1968 _upload_radiocol = UploadManagerRadioCol(self) | |
1969 _upload_avatar = UploadManagerAvatar(self) | |
1970 d = self.bridgeCall("namespacesGet") | 827 d = self.bridgeCall("namespacesGet") |
1971 d.addCallback(self._namespacesGetCb) | 828 d.addCallback(self._namespacesGetCb) |
1972 d.addErrback(self._namespacesGetEb) | 829 d.addErrback(self._namespacesGetEb) |
1973 self.signal_handler.plugRegister(_register) | |
1974 self.bridge.register_signal("connected", self.signal_handler.connected) | |
1975 self.bridge.register_signal("disconnected", self.signal_handler.disconnected) | |
1976 # core | |
1977 for signal_name in [ | |
1978 "presenceUpdate", | |
1979 "messageNew", | |
1980 "subscribe", | |
1981 "contactDeleted", | |
1982 "newContact", | |
1983 "entityDataUpdated", | |
1984 "paramUpdate", | |
1985 ]: | |
1986 self.bridge.register_signal( | |
1987 signal_name, self.signal_handler.getGenericCb(signal_name) | |
1988 ) | |
1989 # XXX: actionNew is handled separately because the handler must manage | |
1990 # security_limit | |
1991 self.bridge.register_signal("actionNew", self.signal_handler.actionNewHandler) | |
1992 # plugins | |
1993 for signal_name in [ | |
1994 "psEvent", | |
1995 "mucRoomJoined", | |
1996 "tarotGameStarted", | |
1997 "tarotGameNew", | |
1998 "tarotGameChooseContrat", | |
1999 "tarotGameShowCards", | |
2000 "tarotGameInvalidCards", | |
2001 "tarotGameCardsPlayed", | |
2002 "tarotGameYourTurn", | |
2003 "tarotGameScore", | |
2004 "tarotGamePlayers", | |
2005 "radiocolStarted", | |
2006 "radiocolPreload", | |
2007 "radiocolPlay", | |
2008 "radiocolNoUpload", | |
2009 "radiocolUploadOk", | |
2010 "radiocolSongRejected", | |
2011 "radiocolPlayers", | |
2012 "mucRoomLeft", | |
2013 "mucRoomUserChangedNick", | |
2014 "chatStateReceived", | |
2015 ]: | |
2016 self.bridge.register_signal( | |
2017 signal_name, self.signal_handler.getGenericCb(signal_name), "plugin" | |
2018 ) | |
2019 | |
2020 # JSON APIs | |
2021 self.putChildSAT("json_signal_api", self.signal_handler) | |
2022 self.putChildSAT("json_api", MethodHandler(self)) | |
2023 self.putChildSAT("register_api", _register) | |
2024 | |
2025 # files upload | |
2026 self.putChildSAT("upload_radiocol", _upload_radiocol) | |
2027 self.putChildSAT("upload_avatar", _upload_avatar) | |
2028 | |
2029 # static pages | |
2030 # FIXME: legacy blog must be removed entirely in 0.8 | |
2031 try: | |
2032 micro_blog = MicroBlog(self) | |
2033 except Exception as e: | |
2034 log.warning(u"Can't load legacy microblog, ignoring it: {reason}".format( | |
2035 reason=e)) | |
2036 else: | |
2037 self.putChildSAT("blog_legacy", micro_blog) | |
2038 self.putChildSAT(C.THEMES_URL, ProtectedFile(self.themes_dir)) | |
2039 | 830 |
2040 # websocket | 831 # websocket |
2041 if self.options["connection_type"] in ("https", "both"): | 832 if self.options["connection_type"] in ("https", "both"): |
2042 wss = websockets.LiberviaPageWSProtocol.getResource(self, secure=True) | 833 wss = websockets.LiberviaPageWSProtocol.getResource(self, secure=True) |
2043 self.putChildAll("wss", wss) | 834 self.putChildAll(b'wss', wss) |
2044 if self.options["connection_type"] in ("http", "both"): | 835 if self.options["connection_type"] in ("http", "both"): |
2045 ws = websockets.LiberviaPageWSProtocol.getResource(self, secure=False) | 836 ws = websockets.LiberviaPageWSProtocol.getResource(self, secure=False) |
2046 self.putChildAll("ws", ws) | 837 self.putChildAll(b'ws', ws) |
2047 | 838 |
2048 ## following signal is needed for cache handling in Libervia pages | 839 ## following signal is needed for cache handling in Libervia pages |
2049 self.bridge.register_signal( | 840 self.bridge.register_signal( |
2050 "psEventRaw", partial(LiberviaPage.onNodeEvent, self), "plugin" | 841 "psEventRaw", partial(LiberviaPage.onNodeEvent, self), "plugin" |
2051 ) | 842 ) |
2064 "progressError", partial(ProgressHandler._signal, "error") | 855 "progressError", partial(ProgressHandler._signal, "error") |
2065 ) | 856 ) |
2066 | 857 |
2067 # media dirs | 858 # media dirs |
2068 # FIXME: get rid of dirname and "/" in C.XXX_DIR | 859 # FIXME: get rid of dirname and "/" in C.XXX_DIR |
2069 self.putChildAll(os.path.dirname(C.MEDIA_DIR), ProtectedFile(self.media_dir)) | 860 self.putChildAll(os.path.dirname(C.MEDIA_DIR).encode('utf-8'), |
861 ProtectedFile(self.media_dir)) | |
2070 self.cache_resource = web_resource.NoResource() | 862 self.cache_resource = web_resource.NoResource() |
2071 self.putChildAll(C.CACHE_DIR, self.cache_resource) | 863 self.putChildAll(C.CACHE_DIR.encode('utf-8'), self.cache_resource) |
2072 | |
2073 # special | |
2074 self.putChildSAT( | |
2075 "radiocol", | |
2076 ProtectedFile(_upload_radiocol.getTmpDir(), defaultType="audio/ogg"), | |
2077 ) # FIXME: We cheat for PoC because we know we are on the same host, so we use | |
2078 # directly upload dir | |
2079 # pyjamas tests, redirected only for dev versions | |
2080 if self.version[-1] == "D": | |
2081 self.putChildSAT("test", web_util.Redirect("/libervia_test.html")) | |
2082 | 864 |
2083 # redirections | 865 # redirections |
2084 for root in self.roots: | 866 for root in self.roots: |
2085 root._initRedirections(self.options) | 867 root._initRedirections(self.options) |
2086 | 868 |
2093 ) | 875 ) |
2094 self.site = server.Site(wrapped) | 876 self.site = server.Site(wrapped) |
2095 self.site.sessionFactory = LiberviaSession | 877 self.site.sessionFactory = LiberviaSession |
2096 | 878 |
2097 def initEb(self, failure): | 879 def initEb(self, failure): |
2098 log.error(_(u"Init error: {msg}").format(msg=failure)) | 880 log.error(_("Init error: {msg}").format(msg=failure)) |
2099 reactor.stop() | 881 reactor.stop() |
2100 return failure | 882 return failure |
2101 | 883 |
2102 def _bridgeCb(self): | 884 def _bridgeCb(self): |
2103 self.bridge.getReady( | 885 self.bridge.getReady( |
2107 self.initialised.addCallback(self.backendReady) | 889 self.initialised.addCallback(self.backendReady) |
2108 self.initialised.addErrback(self.initEb) | 890 self.initialised.addErrback(self.initEb) |
2109 | 891 |
2110 def _bridgeEb(self, failure_): | 892 def _bridgeEb(self, failure_): |
2111 if isinstance(failure_, BridgeExceptionNoService): | 893 if isinstance(failure_, BridgeExceptionNoService): |
2112 print(u"Can't connect to SàT backend, are you sure it's launched ?") | 894 print("Can't connect to SàT backend, are you sure it's launched ?") |
2113 else: | 895 else: |
2114 log.error(u"Can't connect to bridge: {}".format(failure)) | 896 log.error("Can't connect to bridge: {}".format(failure)) |
2115 sys.exit(1) | 897 sys.exit(1) |
2116 | 898 |
2117 @property | 899 @property |
2118 def version(self): | 900 def version(self): |
2119 """Return the short version of Libervia""" | 901 """Return the short version of Libervia""" |
2126 if version[-1] == "D": | 908 if version[-1] == "D": |
2127 # we are in debug version, we add extra data | 909 # we are in debug version, we add extra data |
2128 try: | 910 try: |
2129 return self._version_cache | 911 return self._version_cache |
2130 except AttributeError: | 912 except AttributeError: |
2131 self._version_cache = u"{} ({})".format( | 913 self._version_cache = "{} ({})".format( |
2132 version, utils.getRepositoryData(libervia) | 914 version, utils.getRepositoryData(libervia) |
2133 ) | 915 ) |
2134 return self._version_cache | 916 return self._version_cache |
2135 else: | 917 else: |
2136 return version | 918 return version |
2174 register_with_ext_jid = self.waiting_profiles.getRegisterWithExtJid(profile) | 956 register_with_ext_jid = self.waiting_profiles.getRegisterWithExtJid(profile) |
2175 self.waiting_profiles.purgeRequest(profile) | 957 self.waiting_profiles.purgeRequest(profile) |
2176 session = request.getSession() | 958 session = request.getSession() |
2177 sat_session = session_iface.ISATSession(session) | 959 sat_session = session_iface.ISATSession(session) |
2178 if sat_session.profile: | 960 if sat_session.profile: |
2179 log.error(_(u"/!\\ Session has already a profile, this should NEVER happen!")) | 961 log.error(_("/!\\ Session has already a profile, this should NEVER happen!")) |
2180 raise failure.Failure(exceptions.ConflictError("Already active")) | 962 raise failure.Failure(exceptions.ConflictError("Already active")) |
2181 | 963 |
2182 sat_session.profile = profile | 964 sat_session.profile = profile |
2183 self.prof_connected.add(profile) | 965 self.prof_connected.add(profile) |
2184 cache_dir = os.path.join( | 966 cache_dir = os.path.join( |
2185 self.cache_root_dir, u"profiles", regex.pathEscape(profile) | 967 self.cache_root_dir, "profiles", regex.pathEscape(profile) |
2186 ) | 968 ) |
2187 # FIXME: would be better to have a global /cache URL which redirect to | 969 # FIXME: would be better to have a global /cache URL which redirect to |
2188 # profile's cache directory, without uuid | 970 # profile's cache directory, without uuid |
2189 self.cache_resource.putChild(sat_session.uuid, ProtectedFile(cache_dir)) | 971 self.cache_resource.putChild(sat_session.uuid.encode('utf-8'), |
972 ProtectedFile(cache_dir)) | |
2190 log.debug( | 973 log.debug( |
2191 _(u"profile cache resource added from {uuid} to {path}").format( | 974 _("profile cache resource added from {uuid} to {path}").format( |
2192 uuid=sat_session.uuid, path=cache_dir | 975 uuid=sat_session.uuid, path=cache_dir |
2193 ) | 976 ) |
2194 ) | 977 ) |
2195 | 978 |
2196 def onExpire(): | 979 def onExpire(): |
2197 log.info(u"Session expired (profile={profile})".format(profile=profile)) | 980 log.info("Session expired (profile={profile})".format(profile=profile)) |
2198 self.cache_resource.delEntity(sat_session.uuid) | 981 self.cache_resource.delEntity(sat_session.uuid.encode('utf-8')) |
2199 log.debug( | 982 log.debug( |
2200 _(u"profile cache resource {uuid} deleted").format(uuid=sat_session.uuid) | 983 _("profile cache resource {uuid} deleted").format(uuid=sat_session.uuid) |
2201 ) | 984 ) |
2202 try: | |
2203 # We purge the queue | |
2204 del self.signal_handler.queue[profile] | |
2205 except KeyError: | |
2206 pass | |
2207 # and now we disconnect the profile | 985 # and now we disconnect the profile |
2208 self.bridgeCall("disconnect", profile) | 986 self.bridgeCall("disconnect", profile) |
2209 | 987 |
2210 session.notifyOnExpire(onExpire) | 988 session.notifyOnExpire(onExpire) |
2211 | 989 |
2274 if ( | 1052 if ( |
2275 login_jid is not None and login_jid.user | 1053 login_jid is not None and login_jid.user |
2276 ): # try to create a new sat profile using the XMPP credentials | 1054 ): # try to create a new sat profile using the XMPP credentials |
2277 if not self.options["allow_registration"]: | 1055 if not self.options["allow_registration"]: |
2278 log.warning( | 1056 log.warning( |
2279 u"Trying to register JID account while registration is not " | 1057 "Trying to register JID account while registration is not " |
2280 u"allowed") | 1058 "allowed") |
2281 raise failure.Failure( | 1059 raise failure.Failure( |
2282 exceptions.DataError( | 1060 exceptions.DataError( |
2283 u"JID login while registration is not allowed" | 1061 "JID login while registration is not allowed" |
2284 ) | 1062 ) |
2285 ) | 1063 ) |
2286 profile = login # FIXME: what if there is a resource? | 1064 profile = login # FIXME: what if there is a resource? |
2287 connect_method = "asyncConnectWithXMPPCredentials" | 1065 connect_method = "asyncConnectWithXMPPCredentials" |
2288 register_with_ext_jid = True | 1066 register_with_ext_jid = True |
2305 if sat_session.profile: | 1083 if sat_session.profile: |
2306 # yes, there is | 1084 # yes, there is |
2307 if sat_session.profile != profile: | 1085 if sat_session.profile != profile: |
2308 # it's a different profile, we need to disconnect it | 1086 # it's a different profile, we need to disconnect it |
2309 log.warning(_( | 1087 log.warning(_( |
2310 u"{new_profile} requested login, but {old_profile} was already " | 1088 "{new_profile} requested login, but {old_profile} was already " |
2311 u"connected, disconnecting {old_profile}").format( | 1089 "connected, disconnecting {old_profile}").format( |
2312 old_profile=sat_session.profile, new_profile=profile)) | 1090 old_profile=sat_session.profile, new_profile=profile)) |
2313 self.purgeSession(request) | 1091 self.purgeSession(request) |
2314 | 1092 |
2315 if self.waiting_profiles.getRequest(profile): | 1093 if self.waiting_profiles.getRequest(profile): |
2316 # FIXME: check if and when this can happen | 1094 # FIXME: check if and when this can happen |
2321 connected = yield self.bridgeCall(connect_method, profile, password) | 1099 connected = yield self.bridgeCall(connect_method, profile, password) |
2322 except Exception as failure_: | 1100 except Exception as failure_: |
2323 fault = getattr(failure_, 'classname', None) | 1101 fault = getattr(failure_, 'classname', None) |
2324 self.waiting_profiles.purgeRequest(profile) | 1102 self.waiting_profiles.purgeRequest(profile) |
2325 if fault in ("PasswordError", "ProfileUnknownError"): | 1103 if fault in ("PasswordError", "ProfileUnknownError"): |
2326 log.info(u"Profile {profile} doesn't exist or the submitted password is " | 1104 log.info("Profile {profile} doesn't exist or the submitted password is " |
2327 u"wrong".format( profile=profile)) | 1105 "wrong".format( profile=profile)) |
2328 raise failure.Failure(ValueError(C.PROFILE_AUTH_ERROR)) | 1106 raise failure.Failure(ValueError(C.PROFILE_AUTH_ERROR)) |
2329 elif fault == "SASLAuthError": | 1107 elif fault == "SASLAuthError": |
2330 log.info(u"The XMPP password of profile {profile} is wrong" | 1108 log.info("The XMPP password of profile {profile} is wrong" |
2331 .format(profile=profile)) | 1109 .format(profile=profile)) |
2332 raise failure.Failure(ValueError(C.XMPP_AUTH_ERROR)) | 1110 raise failure.Failure(ValueError(C.XMPP_AUTH_ERROR)) |
2333 elif fault == "NoReply": | 1111 elif fault == "NoReply": |
2334 log.info(_(u"Did not receive a reply (the timeout expired or the " | 1112 log.info(_("Did not receive a reply (the timeout expired or the " |
2335 u"connection is broken)")) | 1113 "connection is broken)")) |
2336 raise exceptions.TimeOutError | 1114 raise exceptions.TimeOutError |
2337 elif fault is None: | 1115 elif fault is None: |
2338 log.info(_(u"Unexepected failure: {failure_}").format(failure_=failure)) | 1116 log.info(_("Unexepected failure: {failure_}").format(failure_=failure)) |
2339 raise failure_ | 1117 raise failure_ |
2340 else: | 1118 else: |
2341 log.error(u'Unmanaged fault class "{fault}" in errback for the ' | 1119 log.error('Unmanaged fault class "{fault}" in errback for the ' |
2342 u'connection of profile {profile}'.format( | 1120 'connection of profile {profile}'.format( |
2343 fault=fault, profile=profile)) | 1121 fault=fault, profile=profile)) |
2344 raise failure.Failure(exceptions.InternalError(fault)) | 1122 raise failure.Failure(exceptions.InternalError(fault)) |
2345 | 1123 |
2346 if connected: | 1124 if connected: |
2347 # profile is already connected in backend | 1125 # profile is already connected in backend |
2351 # yes, session is active | 1129 # yes, session is active |
2352 if sat_session.profile != profile: | 1130 if sat_session.profile != profile: |
2353 # existing session should have been ended above | 1131 # existing session should have been ended above |
2354 # so this line should never be reached | 1132 # so this line should never be reached |
2355 log.error(_( | 1133 log.error(_( |
2356 u"session profile [{session_profile}] differs from login " | 1134 "session profile [{session_profile}] differs from login " |
2357 u"profile [{profile}], this should not happen!") | 1135 "profile [{profile}], this should not happen!") |
2358 .format(session_profile=sat_session.profile, profile=profile)) | 1136 .format(session_profile=sat_session.profile, profile=profile)) |
2359 raise exceptions.InternalError("profile mismatch") | 1137 raise exceptions.InternalError("profile mismatch") |
2360 defer.returnValue(C.SESSION_ACTIVE) | 1138 defer.returnValue(C.SESSION_ACTIVE) |
2361 log.info( | 1139 log.info( |
2362 _( | 1140 _( |
2363 u"profile {profile} was already connected in backend".format( | 1141 "profile {profile} was already connected in backend".format( |
2364 profile=profile | 1142 profile=profile |
2365 ) | 1143 ) |
2366 ) | 1144 ) |
2367 ) | 1145 ) |
2368 # no, we have to create it | 1146 # no, we have to create it |
2384 - C.INTERNAL_ERROR or any unmanaged fault string | 1162 - C.INTERNAL_ERROR or any unmanaged fault string |
2385 @raise PermissionError: registration is now allowed in server configuration | 1163 @raise PermissionError: registration is now allowed in server configuration |
2386 """ | 1164 """ |
2387 if not self.options["allow_registration"]: | 1165 if not self.options["allow_registration"]: |
2388 log.warning( | 1166 log.warning( |
2389 _(u"Registration received while it is not allowed, hack attempt?") | 1167 _("Registration received while it is not allowed, hack attempt?") |
2390 ) | 1168 ) |
2391 raise failure.Failure( | 1169 raise failure.Failure( |
2392 exceptions.PermissionError(u"Registration is not allowed on this server") | 1170 exceptions.PermissionError("Registration is not allowed on this server") |
2393 ) | 1171 ) |
2394 | 1172 |
2395 if ( | 1173 if ( |
2396 not re.match(C.REG_LOGIN_RE, login) | 1174 not re.match(C.REG_LOGIN_RE, login) |
2397 or not re.match(C.REG_EMAIL_RE, email, re.IGNORECASE) | 1175 or not re.match(C.REG_EMAIL_RE, email, re.IGNORECASE) |
2411 return C.INVALID_CERTIFICATE | 1189 return C.INVALID_CERTIFICATE |
2412 elif status == "InternalError": | 1190 elif status == "InternalError": |
2413 return C.INTERNAL_ERROR | 1191 return C.INTERNAL_ERROR |
2414 else: | 1192 else: |
2415 log.error( | 1193 log.error( |
2416 _(u"Unknown registering error status: {status}\n{traceback}").format( | 1194 _("Unknown registering error status: {status}\n{traceback}").format( |
2417 status=status, traceback=failure_.value.message | 1195 status=status, traceback=failure_.value.message |
2418 ) | 1196 ) |
2419 ) | 1197 ) |
2420 return status | 1198 return status |
2421 | 1199 |
2435 | 1213 |
2436 def startService(self): | 1214 def startService(self): |
2437 """Connect the profile for Libervia and start the HTTP(S) server(s)""" | 1215 """Connect the profile for Libervia and start the HTTP(S) server(s)""" |
2438 | 1216 |
2439 def eb(e): | 1217 def eb(e): |
2440 log.error(_(u"Connection failed: %s") % e) | 1218 log.error(_("Connection failed: %s") % e) |
2441 self.stop() | 1219 self.stop() |
2442 | 1220 |
2443 def initOk(__): | 1221 def initOk(__): |
2444 try: | 1222 try: |
2445 connected = self.bridge.isConnected(C.SERVICE_PROFILE) | 1223 connected = self.bridge.isConnected(C.SERVICE_PROFILE) |
2446 except Exception as e: | 1224 except Exception as e: |
2447 # we don't want the traceback | 1225 # we don't want the traceback |
2448 msg = [l for l in unicode(e).split("\n") if l][-1] | 1226 msg = [l for l in str(e).split("\n") if l][-1] |
2449 log.error( | 1227 log.error( |
2450 u"Can't check service profile ({profile}), are you sure it exists ?" | 1228 "Can't check service profile ({profile}), are you sure it exists ?" |
2451 u"\n{error}".format(profile=C.SERVICE_PROFILE, error=msg)) | 1229 "\n{error}".format(profile=C.SERVICE_PROFILE, error=msg)) |
2452 self.stop() | 1230 self.stop() |
2453 return | 1231 return |
2454 if not connected: | 1232 if not connected: |
2455 self.bridge.connect( | 1233 self.bridge.connect( |
2456 C.SERVICE_PROFILE, | 1234 C.SERVICE_PROFILE, |
2466 | 1244 |
2467 ## URLs ## | 1245 ## URLs ## |
2468 | 1246 |
2469 def putChildSAT(self, path, resource): | 1247 def putChildSAT(self, path, resource): |
2470 """Add a child to the sat resource""" | 1248 """Add a child to the sat resource""" |
1249 if not isinstance(path, bytes): | |
1250 raise ValueError("path must be specified in bytes") | |
2471 self.sat_root.putChild(path, resource) | 1251 self.sat_root.putChild(path, resource) |
2472 | 1252 |
2473 def putChildAll(self, path, resource): | 1253 def putChildAll(self, path, resource): |
2474 """Add a child to all vhost root resources""" | 1254 """Add a child to all vhost root resources""" |
1255 if not isinstance(path, bytes): | |
1256 raise ValueError("path must be specified in bytes") | |
2475 # we wrap before calling putChild, to avoid having useless multiple instances | 1257 # we wrap before calling putChild, to avoid having useless multiple instances |
2476 # of the resource | 1258 # of the resource |
2477 # FIXME: check that no information is leaked (c.f. https://twistedmatrix.com/documents/current/web/howto/using-twistedweb.html#request-encoders) | 1259 # FIXME: check that no information is leaked (c.f. https://twistedmatrix.com/documents/current/web/howto/using-twistedweb.html#request-encoders) |
2478 wrapped_res = web_resource.EncodingResourceWrapper( | 1260 wrapped_res = web_resource.EncodingResourceWrapper( |
2479 resource, [server.GzipEncoderFactory()]) | 1261 resource, [server.GzipEncoderFactory()]) |
2489 build_path_elts = [ | 1271 build_path_elts = [ |
2490 config.getConfig(self.main_conf, "", "local_dir"), | 1272 config.getConfig(self.main_conf, "", "local_dir"), |
2491 C.CACHE_DIR, | 1273 C.CACHE_DIR, |
2492 C.LIBERVIA_CACHE, | 1274 C.LIBERVIA_CACHE, |
2493 regex.pathEscape(site_name)] | 1275 regex.pathEscape(site_name)] |
2494 build_path = u"/".join(build_path_elts) | 1276 build_path = "/".join(build_path_elts) |
2495 return os.path.abspath(os.path.expanduser(build_path)) | 1277 return os.path.abspath(os.path.expanduser(build_path)) |
2496 | 1278 |
2497 def getExtBaseURLData(self, request): | 1279 def getExtBaseURLData(self, request): |
2498 """Retrieve external base URL Data | 1280 """Retrieve external base URL Data |
2499 | 1281 |
2520 except TypeError: | 1302 except TypeError: |
2521 # no x-forwarded-server found, we use proxy_host | 1303 # no x-forwarded-server found, we use proxy_host |
2522 proxy_netloc = proxy_host | 1304 proxy_netloc = proxy_host |
2523 else: | 1305 else: |
2524 # if the proxy host has a port, we use it with server name | 1306 # if the proxy host has a port, we use it with server name |
2525 proxy_port = urlparse.urlsplit(u"//{}".format(proxy_host)).port | 1307 proxy_port = urllib.parse.urlsplit("//{}".format(proxy_host)).port |
2526 proxy_netloc = ( | 1308 proxy_netloc = ( |
2527 u"{}:{}".format(proxy_server, proxy_port) | 1309 "{}:{}".format(proxy_server, proxy_port) |
2528 if proxy_port is not None | 1310 if proxy_port is not None |
2529 else proxy_server | 1311 else proxy_server |
2530 ) | 1312 ) |
2531 proxy_netloc = proxy_netloc.decode("utf-8") | 1313 proxy_netloc = proxy_netloc.decode("utf-8") |
2532 try: | 1314 try: |
2538 else: | 1320 else: |
2539 proxy_scheme, proxy_netloc = None, None | 1321 proxy_scheme, proxy_netloc = None, None |
2540 else: | 1322 else: |
2541 proxy_scheme, proxy_netloc = None, None | 1323 proxy_scheme, proxy_netloc = None, None |
2542 | 1324 |
2543 return urlparse.SplitResult( | 1325 return urllib.parse.SplitResult( |
2544 ext_data.scheme or proxy_scheme or url_path.scheme.decode("utf-8"), | 1326 ext_data.scheme or proxy_scheme or url_path.scheme.decode("utf-8"), |
2545 ext_data.netloc or proxy_netloc or url_path.netloc.decode("utf-8"), | 1327 ext_data.netloc or proxy_netloc or url_path.netloc.decode("utf-8"), |
2546 ext_data.path or u"/", | 1328 ext_data.path or "/", |
2547 "", | 1329 "", |
2548 "", | 1330 "", |
2549 ) | 1331 ) |
2550 | 1332 |
2551 def getExtBaseURL(self, request, path="", query="", fragment="", scheme=None): | 1333 def getExtBaseURL(self, request, path="", query="", fragment="", scheme=None): |
2559 @param fragment(unicode): same as for urlsplit.urlsplit | 1341 @param fragment(unicode): same as for urlsplit.urlsplit |
2560 @param scheme(unicode, None): if not None, will override scheme from base URL | 1342 @param scheme(unicode, None): if not None, will override scheme from base URL |
2561 @return (unicode): external URL | 1343 @return (unicode): external URL |
2562 """ | 1344 """ |
2563 split_result = self.getExtBaseURLData(request) | 1345 split_result = self.getExtBaseURLData(request) |
2564 return urlparse.urlunsplit( | 1346 return urllib.parse.urlunsplit( |
2565 ( | 1347 ( |
2566 split_result.scheme.decode("utf-8") if scheme is None else scheme, | 1348 split_result.scheme if scheme is None else scheme, |
2567 split_result.netloc.decode("utf-8"), | 1349 split_result.netloc, |
2568 os.path.join(split_result.path, path), | 1350 os.path.join(split_result.path, path), |
2569 query, | 1351 query, |
2570 fragment, | 1352 fragment, |
2571 ) | 1353 ) |
2572 ) | 1354 ) |
2577 @param vhost_root(web_resource.Resource): root of this virtual host | 1359 @param vhost_root(web_resource.Resource): root of this virtual host |
2578 @param url(unicode): url to check | 1360 @param url(unicode): url to check |
2579 @return (unicode): possibly redirected URL which should link to the same location | 1361 @return (unicode): possibly redirected URL which should link to the same location |
2580 """ | 1362 """ |
2581 inv_redirections = vhost_root.inv_redirections | 1363 inv_redirections = vhost_root.inv_redirections |
2582 url_parts = url.strip(u"/").split(u"/") | 1364 url_parts = url.strip("/").split("/") |
2583 for idx in xrange(len(url), 0, -1): | 1365 for idx in range(len(url), 0, -1): |
2584 test_url = u"/" + u"/".join(url_parts[:idx]) | 1366 test_url = "/" + "/".join(url_parts[:idx]) |
2585 if test_url in inv_redirections: | 1367 if test_url in inv_redirections: |
2586 rem_url = url_parts[idx:] | 1368 rem_url = url_parts[idx:] |
2587 return os.path.join( | 1369 return os.path.join( |
2588 u"/", u"/".join([inv_redirections[test_url]] + rem_url) | 1370 "/", "/".join([inv_redirections[test_url]] + rem_url) |
2589 ) | 1371 ) |
2590 return url | 1372 return url |
2591 | 1373 |
2592 ## Sessions ## | 1374 ## Sessions ## |
2593 | 1375 |
2594 def purgeSession(self, request): | 1376 def purgeSession(self, request): |
2595 """helper method to purge a session during request handling""" | 1377 """helper method to purge a session during request handling""" |
2596 session = request.session | 1378 session = request.session |
2597 if session is not None: | 1379 if session is not None: |
2598 log.debug(_(u"session purge")) | 1380 log.debug(_("session purge")) |
2599 session.expire() | 1381 session.expire() |
2600 # FIXME: not clean but it seems that it's the best way to reset | 1382 # FIXME: not clean but it seems that it's the best way to reset |
2601 # session during request handling | 1383 # session during request handling |
2602 request._secureSession = request._insecureSession = None | 1384 request._secureSession = request._insecureSession = None |
2603 | 1385 |
2624 @param node(unicode): pubsub node | 1406 @param node(unicode): pubsub node |
2625 @return (unicode): affiliation | 1407 @return (unicode): affiliation |
2626 """ | 1408 """ |
2627 sat_session = self.getSessionData(request, session_iface.ISATSession) | 1409 sat_session = self.getSessionData(request, session_iface.ISATSession) |
2628 if sat_session.profile is None: | 1410 if sat_session.profile is None: |
2629 raise exceptions.InternalError(u"profile must be set to use this method") | 1411 raise exceptions.InternalError("profile must be set to use this method") |
2630 affiliation = sat_session.getAffiliation(service, node) | 1412 affiliation = sat_session.getAffiliation(service, node) |
2631 if affiliation is not None: | 1413 if affiliation is not None: |
2632 defer.returnValue(affiliation) | 1414 defer.returnValue(affiliation) |
2633 else: | 1415 else: |
2634 try: | 1416 try: |
2639 log.warning( | 1421 log.warning( |
2640 "Can't retrieve affiliation for {service}/{node}: {reason}".format( | 1422 "Can't retrieve affiliation for {service}/{node}: {reason}".format( |
2641 service=service, node=node, reason=e | 1423 service=service, node=node, reason=e |
2642 ) | 1424 ) |
2643 ) | 1425 ) |
2644 affiliation = u"" | 1426 affiliation = "" |
2645 else: | 1427 else: |
2646 try: | 1428 try: |
2647 affiliation = affiliations[node] | 1429 affiliation = affiliations[node] |
2648 except KeyError: | 1430 except KeyError: |
2649 affiliation = u"" | 1431 affiliation = "" |
2650 sat_session.setAffiliation(service, node, affiliation) | 1432 sat_session.setAffiliation(service, node, affiliation) |
2651 defer.returnValue(affiliation) | 1433 defer.returnValue(affiliation) |
2652 | 1434 |
2653 ## Websocket (dynamic pages) ## | 1435 ## Websocket (dynamic pages) ## |
2654 | 1436 |
2655 def getWebsocketURL(self, request): | 1437 def getWebsocketURL(self, request): |
2656 base_url_split = self.getExtBaseURLData(request) | 1438 base_url_split = self.getExtBaseURLData(request) |
2657 if base_url_split.scheme.endswith("s"): | 1439 if base_url_split.scheme.endswith("s"): |
2658 scheme = u"wss" | 1440 scheme = "wss" |
2659 else: | 1441 else: |
2660 scheme = u"ws" | 1442 scheme = "ws" |
2661 | 1443 |
2662 return self.getExtBaseURL(request, path=scheme, scheme=scheme) | 1444 return self.getExtBaseURL(request, path=scheme, scheme=scheme) |
2663 | 1445 |
2664 def registerWSToken(self, token, page, request): | 1446 def registerWSToken(self, token, page, request): |
2665 # we make a shallow copy of request to avoid losing request.channel when | 1447 # we make a shallow copy of request to avoid losing request.channel when |
2670 | 1452 |
2671 ## Various utils ## | 1453 ## Various utils ## |
2672 | 1454 |
2673 def getHTTPDate(self, timestamp=None): | 1455 def getHTTPDate(self, timestamp=None): |
2674 now = time.gmtime(timestamp) | 1456 now = time.gmtime(timestamp) |
2675 fmt_date = u"{day_name}, %d {month_name} %Y %H:%M:%S GMT".format( | 1457 fmt_date = "{day_name}, %d {month_name} %Y %H:%M:%S GMT".format( |
2676 day_name=C.HTTP_DAYS[now.tm_wday], month_name=C.HTTP_MONTH[now.tm_mon - 1] | 1458 day_name=C.HTTP_DAYS[now.tm_wday], month_name=C.HTTP_MONTH[now.tm_mon - 1] |
2677 ) | 1459 ) |
2678 return time.strftime(fmt_date, now) | 1460 return time.strftime(fmt_date, now) |
2679 | 1461 |
2680 ## TLS related methods ## | 1462 ## TLS related methods ## |
2683 """Check options coherence if TLS is activated, and update missing values | 1465 """Check options coherence if TLS is activated, and update missing values |
2684 | 1466 |
2685 Must be called only if TLS is activated | 1467 Must be called only if TLS is activated |
2686 """ | 1468 """ |
2687 if not self.options["tls_certificate"]: | 1469 if not self.options["tls_certificate"]: |
2688 log.error(u"a TLS certificate is needed to activate HTTPS connection") | 1470 log.error("a TLS certificate is needed to activate HTTPS connection") |
2689 self.quit(1) | 1471 self.quit(1) |
2690 if not self.options["tls_private_key"]: | 1472 if not self.options["tls_private_key"]: |
2691 self.options["tls_private_key"] = self.options["tls_certificate"] | 1473 self.options["tls_private_key"] = self.options["tls_certificate"] |
2692 | 1474 |
2693 if not self.options["tls_private_key"]: | 1475 if not self.options["tls_private_key"]: |
2713 OpenSSL.crypto.FILETYPE_PEM, "".join(buf) | 1495 OpenSSL.crypto.FILETYPE_PEM, "".join(buf) |
2714 ) | 1496 ) |
2715 ) | 1497 ) |
2716 buf = [] | 1498 buf = [] |
2717 elif not line: | 1499 elif not line: |
2718 log.debug(u"{} certificate(s) found".format(len(certificates))) | 1500 log.debug("{} certificate(s) found".format(len(certificates))) |
2719 return certificates | 1501 return certificates |
2720 | 1502 |
2721 def _loadPKey(self, f): | 1503 def _loadPKey(self, f): |
2722 """Read a private key from a .pem file | 1504 """Read a private key from a .pem file |
2723 | 1505 |
2737 return OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, f.read()) | 1519 return OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, f.read()) |
2738 | 1520 |
2739 def _getTLSContextFactory(self): | 1521 def _getTLSContextFactory(self): |
2740 """Load TLS certificate and build the context factory needed for listenSSL""" | 1522 """Load TLS certificate and build the context factory needed for listenSSL""" |
2741 if ssl is None: | 1523 if ssl is None: |
2742 raise ImportError(u"Python module pyOpenSSL is not installed!") | 1524 raise ImportError("Python module pyOpenSSL is not installed!") |
2743 | 1525 |
2744 cert_options = {} | 1526 cert_options = {} |
2745 | 1527 |
2746 for name, option, method in [ | 1528 for name, option, method in [ |
2747 ("privateKey", "tls_private_key", self._loadPKey), | 1529 ("privateKey", "tls_private_key", self._loadPKey), |
2750 ]: | 1532 ]: |
2751 path = self.options[option] | 1533 path = self.options[option] |
2752 if not path: | 1534 if not path: |
2753 assert option == "tls_chain" | 1535 assert option == "tls_chain" |
2754 continue | 1536 continue |
2755 log.debug(u"loading {option} from {path}".format(option=option, path=path)) | 1537 log.debug("loading {option} from {path}".format(option=option, path=path)) |
2756 try: | 1538 try: |
2757 with open(path) as f: | 1539 with open(path) as f: |
2758 cert_options[name] = method(f) | 1540 cert_options[name] = method(f) |
2759 except IOError as e: | 1541 except IOError as e: |
2760 log.error( | 1542 log.error( |
2761 u"Error while reading file {path} for option {option}: {error}".format( | 1543 "Error while reading file {path} for option {option}: {error}".format( |
2762 path=path, option=option, error=e | 1544 path=path, option=option, error=e |
2763 ) | 1545 ) |
2764 ) | 1546 ) |
2765 self.quit(2) | 1547 self.quit(2) |
2766 except OpenSSL.crypto.Error: | 1548 except OpenSSL.crypto.Error: |
2767 log.error( | 1549 log.error( |
2768 u"Error while parsing file {path} for option {option}, are you sure " | 1550 "Error while parsing file {path} for option {option}, are you sure " |
2769 u"it is a valid .pem file?".format( path=path, option=option)) | 1551 "it is a valid .pem file?".format( path=path, option=option)) |
2770 if ( | 1552 if ( |
2771 option == "tls_private_key" | 1553 option == "tls_private_key" |
2772 and self.options["tls_certificate"] == path | 1554 and self.options["tls_certificate"] == path |
2773 ): | 1555 ): |
2774 log.error( | 1556 log.error( |
2775 u"You are using the same file for private key and public " | 1557 "You are using the same file for private key and public " |
2776 u"certificate, make sure that both a in {path} or use " | 1558 "certificate, make sure that both a in {path} or use " |
2777 u"--tls_private_key option".format(path=path)) | 1559 "--tls_private_key option".format(path=path)) |
2778 self.quit(2) | 1560 self.quit(2) |
2779 | 1561 |
2780 return ssl.CertificateOptions(**cert_options) | 1562 return ssl.CertificateOptions(**cert_options) |
2781 | 1563 |
2782 ## service management ## | 1564 ## service management ## |
2788 @raise IOError: the certificate file doesn't exist | 1570 @raise IOError: the certificate file doesn't exist |
2789 @raise OpenSSL.crypto.Error: the certificate file is invalid | 1571 @raise OpenSSL.crypto.Error: the certificate file is invalid |
2790 """ | 1572 """ |
2791 # now that we have service profile connected, we add resource for its cache | 1573 # now that we have service profile connected, we add resource for its cache |
2792 service_path = regex.pathEscape(C.SERVICE_PROFILE) | 1574 service_path = regex.pathEscape(C.SERVICE_PROFILE) |
2793 cache_dir = os.path.join(self.cache_root_dir, u"profiles", service_path) | 1575 cache_dir = os.path.join(self.cache_root_dir, "profiles", service_path) |
2794 self.cache_resource.putChild(service_path, ProtectedFile(cache_dir)) | 1576 self.cache_resource.putChild(service_path.encode('utf-8'), |
2795 self.service_cache_url = u"/" + os.path.join(C.CACHE_DIR, service_path) | 1577 ProtectedFile(cache_dir)) |
1578 self.service_cache_url = "/" + os.path.join(C.CACHE_DIR, service_path) | |
2796 session_iface.SATSession.service_cache_url = self.service_cache_url | 1579 session_iface.SATSession.service_cache_url = self.service_cache_url |
2797 | 1580 |
2798 if self.options["connection_type"] in ("https", "both"): | 1581 if self.options["connection_type"] in ("https", "both"): |
2799 self._TLSOptionsCheck() | 1582 self._TLSOptionsCheck() |
2800 context_factory = self._getTLSContextFactory() | 1583 context_factory = self._getTLSContextFactory() |
2821 for callback, args, kwargs in self._cleanup: | 1604 for callback, args, kwargs in self._cleanup: |
2822 callback(*args, **kwargs) | 1605 callback(*args, **kwargs) |
2823 try: | 1606 try: |
2824 yield self.bridgeCall("disconnect", C.SERVICE_PROFILE) | 1607 yield self.bridgeCall("disconnect", C.SERVICE_PROFILE) |
2825 except Exception: | 1608 except Exception: |
2826 log.warning(u"Can't disconnect service profile") | 1609 log.warning("Can't disconnect service profile") |
2827 | 1610 |
2828 def run(self): | 1611 def run(self): |
2829 reactor.run() | 1612 reactor.run() |
2830 | 1613 |
2831 def stop(self): | 1614 def stop(self): |