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>&nbsp;</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>&nbsp;</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>&nbsp;</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):