Mercurial > libervia-web
comparison libervia/server/server.py @ 1124:28e3eb3bb217
files reorganisation and installation rework:
- files have been reorganised to follow other SàT projects and usual Python organisation (no more "/src" directory)
- VERSION file is now used, as for other SàT projects
- replace the overcomplicated setup.py be a more sane one. Pyjamas part is not compiled anymore by setup.py, it must be done separatly
- removed check for data_dir if it's empty
- installation tested working in virtual env
- libervia launching script is now in bin/libervia
author | Goffi <goffi@goffi.org> |
---|---|
date | Sat, 25 Aug 2018 17:59:48 +0200 |
parents | src/server/server.py@cdd389ef97bc |
children | 7cd89277a129 |
comparison
equal
deleted
inserted
replaced
1123:63a4b8fe9782 | 1124:28e3eb3bb217 |
---|---|
1 #!/usr/bin/python | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # Libervia: a Salut à Toi frontend | |
5 # Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 from twisted.application import service | |
21 from twisted.internet import reactor, defer | |
22 from twisted.web import server | |
23 from twisted.web import static | |
24 from twisted.web import resource as web_resource | |
25 from twisted.web import util as web_util | |
26 from twisted.web import http | |
27 from twisted.python.components import registerAdapter | |
28 from twisted.python import failure | |
29 from twisted.words.protocols.jabber import jid | |
30 | |
31 from txjsonrpc.web import jsonrpc | |
32 from txjsonrpc import jsonrpclib | |
33 | |
34 from sat.core.log import getLogger | |
35 | |
36 log = getLogger(__name__) | |
37 from sat_frontends.bridge.dbus_bridge import ( | |
38 Bridge, | |
39 BridgeExceptionNoService, | |
40 const_TIMEOUT as BRIDGE_TIMEOUT, | |
41 ) | |
42 from sat.core.i18n import _, D_ | |
43 from sat.core import exceptions | |
44 from sat.tools import utils | |
45 from sat.tools.common import regex | |
46 from sat.tools.common import template | |
47 | |
48 import re | |
49 import glob | |
50 import os.path | |
51 import sys | |
52 import tempfile | |
53 import shutil | |
54 import uuid | |
55 import urlparse | |
56 import urllib | |
57 import time | |
58 from httplib import HTTPS_PORT | |
59 import libervia | |
60 from libervia.server import websockets | |
61 from libervia.server.pages import LiberviaPage | |
62 from libervia.server.utils import quote, ProgressHandler | |
63 from functools import partial | |
64 | |
65 try: | |
66 import OpenSSL | |
67 from twisted.internet import ssl | |
68 except ImportError: | |
69 ssl = None | |
70 | |
71 from libervia.server.constants import Const as C | |
72 from libervia.server.blog import MicroBlog | |
73 from libervia.server import session_iface | |
74 | |
75 | |
76 # following value are set from twisted.plugins.libervia_server initialise (see the comment there) | |
77 DATA_DIR_DEFAULT = OPT_PARAMETERS_BOTH = OPT_PARAMETERS_CFG = coerceDataDir = None | |
78 | |
79 | |
80 class LiberviaSession(server.Session): | |
81 sessionTimeout = C.SESSION_TIMEOUT | |
82 | |
83 def __init__(self, *args, **kwargs): | |
84 self.__lock = False | |
85 server.Session.__init__(self, *args, **kwargs) | |
86 | |
87 def lock(self): | |
88 """Prevent session from expiring""" | |
89 self.__lock = True | |
90 self._expireCall.reset(sys.maxint) | |
91 | |
92 def unlock(self): | |
93 """Allow session to expire again, and touch it""" | |
94 self.__lock = False | |
95 self.touch() | |
96 | |
97 def touch(self): | |
98 if not self.__lock: | |
99 server.Session.touch(self) | |
100 | |
101 | |
102 class ProtectedFile(static.File): | |
103 """A static.File class which doens't show directory listing""" | |
104 | |
105 def directoryListing(self): | |
106 return web_resource.NoResource() | |
107 | |
108 | |
109 class LiberviaRootResource(ProtectedFile): | |
110 """Specialized resource for Libervia root | |
111 | |
112 handle redirections declared in sat.conf | |
113 """ | |
114 | |
115 def _initRedirections(self, options): | |
116 ## redirections | |
117 self.redirections = {} | |
118 self.inv_redirections = {} # new URL to old URL map | |
119 | |
120 if options["url_redirections_dict"] and not options["url_redirections_profile"]: | |
121 # FIXME: url_redirections_profile should not be needed. It is currently used to | |
122 # redirect to an URL which associate the profile with the service, but this | |
123 # is not clean, and service should be explicitly specified | |
124 raise ValueError( | |
125 u"url_redirections_profile need to be filled if you want to use url_redirections_dict" | |
126 ) | |
127 | |
128 for old, new_data in options["url_redirections_dict"].iteritems(): | |
129 # new_data can be a dictionary or a unicode url | |
130 if isinstance(new_data, dict): | |
131 # new_data dict must contain either "url", "page" or "path" key (exclusive) | |
132 # if "path" is used, a file url is constructed with it | |
133 if len({"path", "url", "page"}.intersection(new_data.keys())) != 1: | |
134 raise ValueError( | |
135 u'You must have one and only one of "url", "page" or "path" key in your url_redirections_dict data' | |
136 ) | |
137 if "url" in new_data: | |
138 new = new_data["url"] | |
139 elif "page" in new_data: | |
140 new = new_data | |
141 new["type"] = "page" | |
142 new.setdefault("path_args", []) | |
143 if not isinstance(new["path_args"], list): | |
144 log.error( | |
145 _( | |
146 u'"path_args" in redirection of {old} must be a list. Ignoring the redirection'.format( | |
147 old=old | |
148 ) | |
149 ) | |
150 ) | |
151 continue | |
152 new.setdefault("query_args", {}) | |
153 if not isinstance(new["query_args"], dict): | |
154 log.error( | |
155 _( | |
156 u'"query_args" in redirection of {old} must be a dictionary. Ignoring the redirection'.format( | |
157 old=old | |
158 ) | |
159 ) | |
160 ) | |
161 continue | |
162 new["path_args"] = [quote(a) for a in new["path_args"]] | |
163 # we keep an inversed dict of page redirection (page/path_args => redirecting URL) | |
164 # so getURL can return the redirecting URL if the same arguments are used | |
165 # making the URL consistent | |
166 args_hash = tuple(new["path_args"]) | |
167 LiberviaPage.pages_redirects.setdefault(new_data["page"], {})[ | |
168 args_hash | |
169 ] = old | |
170 | |
171 # we need lists in query_args because it will be used | |
172 # as it in request.path_args | |
173 for k, v in new["query_args"].iteritems(): | |
174 if isinstance(v, basestring): | |
175 new["query_args"][k] = [v] | |
176 elif "path" in new_data: | |
177 new = "file:{}".format(urllib.quote(new_data["path"])) | |
178 elif isinstance(new_data, basestring): | |
179 new = new_data | |
180 new_data = {} | |
181 else: | |
182 log.error( | |
183 _(u"ignoring invalid redirection value: {new_data}").format( | |
184 new_data=new_data | |
185 ) | |
186 ) | |
187 continue | |
188 | |
189 # some normalization | |
190 if not old.strip(): | |
191 # root URL special case | |
192 old = "" | |
193 elif not old.startswith("/"): | |
194 log.error( | |
195 _( | |
196 u"redirected url must start with '/', got {value}. Ignoring" | |
197 ).format(value=old) | |
198 ) | |
199 continue | |
200 else: | |
201 old = self._normalizeURL(old) | |
202 | |
203 if isinstance(new, dict): | |
204 # dict are handled differently, they contain data | |
205 # which ared use dynamically when the request is done | |
206 self.redirections[old] = new | |
207 if not old: | |
208 if new[u"type"] == u"page": | |
209 log.info( | |
210 _(u"Root URL redirected to page {name}").format( | |
211 name=new[u"page"] | |
212 ) | |
213 ) | |
214 else: | |
215 if new[u"type"] == u"page": | |
216 page = LiberviaPage.getPageByName(new[u"page"]) | |
217 url = page.getURL(*new.get(u"path_args", [])) | |
218 self.inv_redirections[url] = old | |
219 continue | |
220 | |
221 # at this point we have a redirection URL in new, we can parse it | |
222 new_url = urlparse.urlsplit(new.encode("utf-8")) | |
223 | |
224 # we handle the known URL schemes | |
225 if new_url.scheme == "xmpp": | |
226 location = LiberviaPage.getPagePathFromURI(new) | |
227 if location is None: | |
228 log.warning( | |
229 _( | |
230 u"ignoring redirection, no page found to handle this URI: {uri}" | |
231 ).format(uri=new) | |
232 ) | |
233 continue | |
234 request_data = self._getRequestData(location) | |
235 if old: | |
236 self.inv_redirections[location] = old | |
237 | |
238 elif new_url.scheme in ("", "http", "https"): | |
239 # direct redirection | |
240 if new_url.netloc: | |
241 raise NotImplementedError( | |
242 u"netloc ({netloc}) is not implemented yet for url_redirections_dict, it is not possible to redirect to an external website".format( | |
243 netloc=new_url.netloc | |
244 ) | |
245 ) | |
246 location = urlparse.urlunsplit( | |
247 ("", "", new_url.path, new_url.query, new_url.fragment) | |
248 ).decode("utf-8") | |
249 request_data = self._getRequestData(location) | |
250 if old: | |
251 self.inv_redirections[location] = old | |
252 | |
253 elif new_url.scheme in ("file"): | |
254 # file or directory | |
255 if new_url.netloc: | |
256 raise NotImplementedError( | |
257 u"netloc ({netloc}) is not implemented for url redirection to file system, it is not possible to redirect to an external host".format( | |
258 netloc=new_url.netloc | |
259 ) | |
260 ) | |
261 path = urllib.unquote(new_url.path) | |
262 if not os.path.isabs(path): | |
263 raise ValueError( | |
264 u"file redirection must have an absolute path: e.g. file:/path/to/my/file" | |
265 ) | |
266 # for file redirection, we directly put child here | |
267 segments, dummy, last_segment = old.rpartition("/") | |
268 url_segments = segments.split("/") if segments else [] | |
269 current = self | |
270 for segment in url_segments: | |
271 resource = web_resource.NoResource() | |
272 current.putChild(segment, resource) | |
273 current = resource | |
274 resource_class = ( | |
275 ProtectedFile if new_data.get("protected", True) else static.File | |
276 ) | |
277 current.putChild(last_segment, resource_class(path)) | |
278 log.info( | |
279 u"Added redirection from /{old} to file system path {path}".format( | |
280 old=old.decode("utf-8"), path=path.decode("utf-8") | |
281 ) | |
282 ) | |
283 continue # we don't want to use redirection system, so we continue here | |
284 | |
285 else: | |
286 raise NotImplementedError( | |
287 u"{scheme}: scheme is not managed for url_redirections_dict".format( | |
288 scheme=new_url.scheme | |
289 ) | |
290 ) | |
291 | |
292 self.redirections[old] = request_data | |
293 if not old: | |
294 log.info( | |
295 _(u"Root URL redirected to {uri}").format( | |
296 uri=request_data[1].decode("utf-8") | |
297 ) | |
298 ) | |
299 | |
300 # no need to keep url_redirections*, they will not be used anymore | |
301 del options["url_redirections_dict"] | |
302 del options["url_redirections_profile"] | |
303 | |
304 # the default root URL, if not redirected | |
305 if not "" in self.redirections: | |
306 self.redirections[""] = self._getRequestData(C.LIBERVIA_MAIN_PAGE) | |
307 | |
308 def _normalizeURL(self, url, lower=True): | |
309 """Return URL normalized for self.redirections dict | |
310 | |
311 @param url(unicode): URL to normalize | |
312 @param lower(bool): lower case of url if True | |
313 @return (str): normalized URL | |
314 """ | |
315 if lower: | |
316 url = url.lower() | |
317 return "/".join((p for p in url.encode("utf-8").split("/") if p)) | |
318 | |
319 def _getRequestData(self, uri): | |
320 """Return data needed to redirect request | |
321 | |
322 @param url(unicode): destination url | |
323 @return (tuple(list[str], str, str, dict): tuple with | |
324 splitted path as in Request.postpath | |
325 uri as in Request.uri | |
326 path as in Request.path | |
327 args as in Request.args | |
328 """ | |
329 uri = uri.encode("utf-8") | |
330 # XXX: we reuse code from twisted.web.http.py here | |
331 # as we need to have the same behaviour | |
332 x = uri.split(b"?", 1) | |
333 | |
334 if len(x) == 1: | |
335 path = uri | |
336 args = {} | |
337 else: | |
338 path, argstring = x | |
339 args = http.parse_qs(argstring, 1) | |
340 | |
341 # XXX: splitted path case must not be changed, as it may be significant | |
342 # (e.g. for blog items) | |
343 return ( | |
344 self._normalizeURL(path.decode("utf-8"), lower=False).split("/"), | |
345 uri, | |
346 path, | |
347 args, | |
348 ) | |
349 | |
350 def _redirect(self, request, request_data): | |
351 """Redirect an URL by rewritting request | |
352 | |
353 this is *NOT* a HTTP redirection, but equivalent to URL rewritting | |
354 @param request(web.http.request): original request | |
355 @param request_data(tuple): data returned by self._getRequestData | |
356 @return (web_resource.Resource): resource to use | |
357 """ | |
358 # recursion check | |
359 try: | |
360 request._redirected | |
361 except AttributeError: | |
362 pass | |
363 else: | |
364 try: | |
365 dummy, uri, dummy, dummy = request_data | |
366 except ValueError: | |
367 uri = u"" | |
368 log.error( | |
369 D_( | |
370 u"recursive redirection, please fix this URL:\n{old} ==> {new}" | |
371 ).format(old=request.uri.decode("utf-8"), new=uri.decode("utf-8")) | |
372 ) | |
373 return web_resource.NoResource() | |
374 | |
375 request._redirected = True # here to avoid recursive redirections | |
376 | |
377 if isinstance(request_data, dict): | |
378 if request_data["type"] == "page": | |
379 try: | |
380 page = LiberviaPage.getPageByName(request_data["page"]) | |
381 except KeyError: | |
382 log.error( | |
383 _( | |
384 u'Can\'t find page named "{name}" requested in redirection' | |
385 ).format(name=request_data["page"]) | |
386 ) | |
387 return web_resource.NoResource() | |
388 request.postpath = request_data["path_args"][:] + request.postpath | |
389 | |
390 try: | |
391 request.args.update(request_data["query_args"]) | |
392 except (TypeError, ValueError): | |
393 log.error( | |
394 _(u"Invalid args in redirection: {query_args}").format( | |
395 query_args=request_data["query_args"] | |
396 ) | |
397 ) | |
398 return web_resource.NoResource() | |
399 return page | |
400 else: | |
401 raise exceptions.InternalError(u"unknown request_data type") | |
402 else: | |
403 path_list, uri, path, args = request_data | |
404 log.debug( | |
405 u"Redirecting URL {old} to {new}".format( | |
406 old=request.uri.decode("utf-8"), new=uri.decode("utf-8") | |
407 ) | |
408 ) | |
409 # we change the request to reflect the new url | |
410 request.postpath = path_list[1:] + request.postpath | |
411 request.args = args | |
412 | |
413 # we start again to look for a child with the new url | |
414 return self.getChildWithDefault(path_list[0], request) | |
415 | |
416 def getChildWithDefault(self, name, request): | |
417 # XXX: this method is overriden only for root url | |
418 # which is the only ones who need to be handled before other children | |
419 if name == "" and not request.postpath: | |
420 return self._redirect(request, self.redirections[""]) | |
421 return super(LiberviaRootResource, self).getChildWithDefault(name, request) | |
422 | |
423 def getChild(self, name, request): | |
424 resource = super(LiberviaRootResource, self).getChild(name, request) | |
425 | |
426 if isinstance(resource, web_resource.NoResource): | |
427 # if nothing was found, we try our luck with redirections | |
428 # XXX: we want redirections to happen only if everything else failed | |
429 path_elt = request.prepath + request.postpath | |
430 for idx in xrange(len(path_elt), 0, -1): | |
431 test_url = "/".join(path_elt[:idx]).lower() | |
432 if test_url in self.redirections: | |
433 request_data = self.redirections[test_url] | |
434 request.postpath = path_elt[idx:] | |
435 return self._redirect(request, request_data) | |
436 | |
437 return resource | |
438 | |
439 def createSimilarFile(self, path): | |
440 # XXX: this method need to be overriden to avoid recreating a LiberviaRootResource | |
441 | |
442 f = LiberviaRootResource.__base__( | |
443 path, self.defaultType, self.ignoredExts, self.registry | |
444 ) | |
445 # refactoring by steps, here - constructor should almost certainly take these | |
446 f.processors = self.processors | |
447 f.indexNames = self.indexNames[:] | |
448 f.childNotFound = self.childNotFound | |
449 return f | |
450 | |
451 | |
452 class JSONRPCMethodManager(jsonrpc.JSONRPC): | |
453 def __init__(self, sat_host): | |
454 jsonrpc.JSONRPC.__init__(self) | |
455 self.sat_host = sat_host | |
456 | |
457 def asyncBridgeCall(self, method_name, *args, **kwargs): | |
458 return self.sat_host.bridgeCall(method_name, *args, **kwargs) | |
459 | |
460 | |
461 class MethodHandler(JSONRPCMethodManager): | |
462 def __init__(self, sat_host): | |
463 JSONRPCMethodManager.__init__(self, sat_host) | |
464 | |
465 def render(self, request): | |
466 self.session = request.getSession() | |
467 profile = session_iface.ISATSession(self.session).profile | |
468 if not profile: | |
469 # user is not identified, we return a jsonrpc fault | |
470 parsed = jsonrpclib.loads(request.content.read()) | |
471 fault = jsonrpclib.Fault( | |
472 C.ERRNUM_LIBERVIA, C.NOT_ALLOWED | |
473 ) # FIXME: define some standard error codes for libervia | |
474 return jsonrpc.JSONRPC._cbRender( | |
475 self, fault, request, parsed.get("id"), parsed.get("jsonrpc") | |
476 ) # pylint: disable=E1103 | |
477 return jsonrpc.JSONRPC.render(self, request) | |
478 | |
479 @defer.inlineCallbacks | |
480 def jsonrpc_getVersion(self): | |
481 """Return SàT version""" | |
482 try: | |
483 defer.returnValue(self._version_cache) | |
484 except AttributeError: | |
485 self._version_cache = yield self.sat_host.bridgeCall("getVersion") | |
486 defer.returnValue(self._version_cache) | |
487 | |
488 def jsonrpc_getLiberviaVersion(self): | |
489 """Return Libervia version""" | |
490 return self.sat_host.full_version | |
491 | |
492 def jsonrpc_disconnect(self): | |
493 """Disconnect the profile""" | |
494 sat_session = session_iface.ISATSession(self.session) | |
495 profile = sat_session.profile | |
496 self.sat_host.bridgeCall("disconnect", profile) | |
497 | |
498 def jsonrpc_getContacts(self): | |
499 """Return all passed args.""" | |
500 profile = session_iface.ISATSession(self.session).profile | |
501 return self.sat_host.bridgeCall("getContacts", profile) | |
502 | |
503 @defer.inlineCallbacks | |
504 def jsonrpc_addContact(self, entity, name, groups): | |
505 """Subscribe to contact presence, and add it to the given groups""" | |
506 profile = session_iface.ISATSession(self.session).profile | |
507 yield self.sat_host.bridgeCall("addContact", entity, profile) | |
508 yield self.sat_host.bridgeCall("updateContact", entity, name, groups, profile) | |
509 | |
510 def jsonrpc_delContact(self, entity): | |
511 """Remove contact from contacts list""" | |
512 profile = session_iface.ISATSession(self.session).profile | |
513 return self.sat_host.bridgeCall("delContact", entity, profile) | |
514 | |
515 def jsonrpc_updateContact(self, entity, name, groups): | |
516 """Update contact's roster item""" | |
517 profile = session_iface.ISATSession(self.session).profile | |
518 return self.sat_host.bridgeCall("updateContact", entity, name, groups, profile) | |
519 | |
520 def jsonrpc_subscription(self, sub_type, entity): | |
521 """Confirm (or infirm) subscription, | |
522 and setup user roster in case of subscription""" | |
523 profile = session_iface.ISATSession(self.session).profile | |
524 return self.sat_host.bridgeCall("subscription", sub_type, entity, profile) | |
525 | |
526 def jsonrpc_getWaitingSub(self): | |
527 """Return list of room already joined by user""" | |
528 profile = session_iface.ISATSession(self.session).profile | |
529 return self.sat_host.bridgeCall("getWaitingSub", profile) | |
530 | |
531 def jsonrpc_setStatus(self, presence, status): | |
532 """Change the presence and/or status | |
533 @param presence: value from ("", "chat", "away", "dnd", "xa") | |
534 @param status: any string to describe your status | |
535 """ | |
536 profile = session_iface.ISATSession(self.session).profile | |
537 return self.sat_host.bridgeCall( | |
538 "setPresence", "", presence, {"": status}, profile | |
539 ) | |
540 | |
541 def jsonrpc_messageSend(self, to_jid, msg, subject, type_, extra={}): | |
542 """send message""" | |
543 profile = session_iface.ISATSession(self.session).profile | |
544 return self.asyncBridgeCall( | |
545 "messageSend", to_jid, msg, subject, type_, extra, profile | |
546 ) | |
547 | |
548 ## PubSub ## | |
549 | |
550 def jsonrpc_psNodeDelete(self, service, node): | |
551 """Delete a whole node | |
552 | |
553 @param service (unicode): service jid | |
554 @param node (unicode): node to delete | |
555 """ | |
556 profile = session_iface.ISATSession(self.session).profile | |
557 return self.asyncBridgeCall("psNodeDelete", service, node, profile) | |
558 | |
559 # def jsonrpc_psRetractItem(self, service, node, item, notify): | |
560 # """Delete a whole node | |
561 | |
562 # @param service (unicode): service jid | |
563 # @param node (unicode): node to delete | |
564 # @param items (iterable): id of item to retract | |
565 # @param notify (bool): True if notification is required | |
566 # """ | |
567 # profile = session_iface.ISATSession(self.session).profile | |
568 # return self.asyncBridgeCall("psRetractItem", service, node, item, notify, profile) | |
569 | |
570 # def jsonrpc_psRetractItems(self, service, node, items, notify): | |
571 # """Delete a whole node | |
572 | |
573 # @param service (unicode): service jid | |
574 # @param node (unicode): node to delete | |
575 # @param items (iterable): ids of items to retract | |
576 # @param notify (bool): True if notification is required | |
577 # """ | |
578 # profile = session_iface.ISATSession(self.session).profile | |
579 # return self.asyncBridgeCall("psRetractItems", service, node, items, notify, profile) | |
580 | |
581 ## microblogging ## | |
582 | |
583 def jsonrpc_mbSend(self, service, node, mb_data): | |
584 """Send microblog data | |
585 | |
586 @param service (unicode): service jid or empty string to use profile's microblog | |
587 @param node (unicode): publishing node, or empty string to use microblog node | |
588 @param mb_data(dict): microblog data | |
589 @return: a deferred | |
590 """ | |
591 profile = session_iface.ISATSession(self.session).profile | |
592 return self.asyncBridgeCall("mbSend", service, node, mb_data, profile) | |
593 | |
594 def jsonrpc_mbRetract(self, service, node, items): | |
595 """Delete a whole node | |
596 | |
597 @param service (unicode): service jid, empty string for PEP | |
598 @param node (unicode): node to delete, empty string for default node | |
599 @param items (iterable): ids of items to retract | |
600 """ | |
601 profile = session_iface.ISATSession(self.session).profile | |
602 return self.asyncBridgeCall("mbRetract", service, node, items, profile) | |
603 | |
604 def jsonrpc_mbGet(self, service_jid, node, max_items, item_ids, extra): | |
605 """Get last microblogs from publisher_jid | |
606 | |
607 @param service_jid (unicode): pubsub service, usually publisher jid | |
608 @param node(unicode): mblogs node, or empty string to get the defaut one | |
609 @param max_items (int): maximum number of item to get or C.NO_LIMIT to get everything | |
610 @param item_ids (list[unicode]): list of item IDs | |
611 @param rsm (dict): TODO | |
612 @return: a deferred couple with the list of items and metadatas. | |
613 """ | |
614 profile = session_iface.ISATSession(self.session).profile | |
615 return self.asyncBridgeCall( | |
616 "mbGet", service_jid, node, max_items, item_ids, extra, profile | |
617 ) | |
618 | |
619 def jsonrpc_mbGetFromMany(self, publishers_type, publishers, max_items, extra): | |
620 """Get many blog nodes at once | |
621 | |
622 @param publishers_type (unicode): one of "ALL", "GROUP", "JID" | |
623 @param publishers (tuple(unicode)): tuple of publishers (empty list for all, list of groups or list of jids) | |
624 @param max_items (int): maximum number of item to get or C.NO_LIMIT to get everything | |
625 @param extra (dict): TODO | |
626 @return (str): RT Deferred session id | |
627 """ | |
628 profile = session_iface.ISATSession(self.session).profile | |
629 return self.sat_host.bridgeCall( | |
630 "mbGetFromMany", publishers_type, publishers, max_items, extra, profile | |
631 ) | |
632 | |
633 def jsonrpc_mbGetFromManyRTResult(self, rt_session): | |
634 """Get results from RealTime mbGetFromMany session | |
635 | |
636 @param rt_session (str): RT Deferred session id | |
637 """ | |
638 profile = session_iface.ISATSession(self.session).profile | |
639 return self.asyncBridgeCall("mbGetFromManyRTResult", rt_session, profile) | |
640 | |
641 def jsonrpc_mbGetFromManyWithComments( | |
642 self, | |
643 publishers_type, | |
644 publishers, | |
645 max_items, | |
646 max_comments, | |
647 rsm_dict, | |
648 rsm_comments_dict, | |
649 ): | |
650 """Helper method to get the microblogs and their comments in one shot | |
651 | |
652 @param publishers_type (str): type of the list of publishers (one of "GROUP" or "JID" or "ALL") | |
653 @param publishers (list): list of publishers, according to publishers_type (list of groups or list of jids) | |
654 @param max_items (int): optional limit on the number of retrieved items. | |
655 @param max_comments (int): maximum number of comments to retrieve | |
656 @param rsm_dict (dict): RSM data for initial items only | |
657 @param rsm_comments_dict (dict): RSM data for comments only | |
658 @param profile_key: profile key | |
659 @return (str): RT Deferred session id | |
660 """ | |
661 profile = session_iface.ISATSession(self.session).profile | |
662 return self.sat_host.bridgeCall( | |
663 "mbGetFromManyWithComments", | |
664 publishers_type, | |
665 publishers, | |
666 max_items, | |
667 max_comments, | |
668 rsm_dict, | |
669 rsm_comments_dict, | |
670 profile, | |
671 ) | |
672 | |
673 def jsonrpc_mbGetFromManyWithCommentsRTResult(self, rt_session): | |
674 """Get results from RealTime mbGetFromManyWithComments session | |
675 | |
676 @param rt_session (str): RT Deferred session id | |
677 """ | |
678 profile = session_iface.ISATSession(self.session).profile | |
679 return self.asyncBridgeCall( | |
680 "mbGetFromManyWithCommentsRTResult", rt_session, profile | |
681 ) | |
682 | |
683 # def jsonrpc_sendMblog(self, type_, dest, text, extra={}): | |
684 # """ Send microblog message | |
685 # @param type_ (unicode): one of "PUBLIC", "GROUP" | |
686 # @param dest (tuple(unicode)): recipient groups (ignored for "PUBLIC") | |
687 # @param text (unicode): microblog's text | |
688 # """ | |
689 # profile = session_iface.ISATSession(self.session).profile | |
690 # extra['allow_comments'] = 'True' | |
691 | |
692 # if not type_: # auto-detect | |
693 # type_ = "PUBLIC" if dest == [] else "GROUP" | |
694 | |
695 # if type_ in ("PUBLIC", "GROUP") and text: | |
696 # if type_ == "PUBLIC": | |
697 # #This text if for the public microblog | |
698 # log.debug("sending public blog") | |
699 # return self.sat_host.bridge.sendGroupBlog("PUBLIC", (), text, extra, profile) | |
700 # else: | |
701 # log.debug("sending group blog") | |
702 # dest = dest if isinstance(dest, list) else [dest] | |
703 # return self.sat_host.bridge.sendGroupBlog("GROUP", dest, text, extra, profile) | |
704 # else: | |
705 # raise Exception("Invalid data") | |
706 | |
707 # def jsonrpc_deleteMblog(self, pub_data, comments): | |
708 # """Delete a microblog node | |
709 # @param pub_data: a tuple (service, comment node identifier, item identifier) | |
710 # @param comments: comments node identifier (for main item) or False | |
711 # """ | |
712 # profile = session_iface.ISATSession(self.session).profile | |
713 # return self.sat_host.bridge.deleteGroupBlog(pub_data, comments if comments else '', profile) | |
714 | |
715 # def jsonrpc_updateMblog(self, pub_data, comments, message, extra={}): | |
716 # """Modify a microblog node | |
717 # @param pub_data: a tuple (service, comment node identifier, item identifier) | |
718 # @param comments: comments node identifier (for main item) or False | |
719 # @param message: new message | |
720 # @param extra: dict which option name as key, which can be: | |
721 # - allow_comments: True to accept an other level of comments, False else (default: False) | |
722 # - rich: if present, contain rich text in currently selected syntax | |
723 # """ | |
724 # profile = session_iface.ISATSession(self.session).profile | |
725 # if comments: | |
726 # extra['allow_comments'] = 'True' | |
727 # return self.sat_host.bridge.updateGroupBlog(pub_data, comments if comments else '', message, extra, profile) | |
728 | |
729 # def jsonrpc_sendMblogComment(self, node, text, extra={}): | |
730 # """ Send microblog message | |
731 # @param node: url of the comments node | |
732 # @param text: comment | |
733 # """ | |
734 # profile = session_iface.ISATSession(self.session).profile | |
735 # if node and text: | |
736 # return self.sat_host.bridge.sendGroupBlogComment(node, text, extra, profile) | |
737 # else: | |
738 # raise Exception("Invalid data") | |
739 | |
740 # def jsonrpc_getMblogs(self, publisher_jid, item_ids, max_items=C.RSM_MAX_ITEMS): | |
741 # """Get specified microblogs posted by a contact | |
742 # @param publisher_jid: jid of the publisher | |
743 # @param item_ids: list of microblogs items IDs | |
744 # @return list of microblog data (dict)""" | |
745 # profile = session_iface.ISATSession(self.session).profile | |
746 # d = self.asyncBridgeCall("getGroupBlogs", publisher_jid, item_ids, {'max_': unicode(max_items)}, False, profile) | |
747 # return d | |
748 | |
749 # def jsonrpc_getMblogsWithComments(self, publisher_jid, item_ids, max_comments=C.RSM_MAX_COMMENTS): | |
750 # """Get specified microblogs posted by a contact and their comments | |
751 # @param publisher_jid: jid of the publisher | |
752 # @param item_ids: list of microblogs items IDs | |
753 # @return list of couple (microblog data, list of microblog data)""" | |
754 # profile = session_iface.ISATSession(self.session).profile | |
755 # d = self.asyncBridgeCall("getGroupBlogsWithComments", publisher_jid, item_ids, {}, max_comments, profile) | |
756 # return d | |
757 | |
758 # def jsonrpc_getMassiveMblogs(self, publishers_type, publishers, rsm=None): | |
759 # """Get lasts microblogs posted by several contacts at once | |
760 | |
761 # @param publishers_type (unicode): one of "ALL", "GROUP", "JID" | |
762 # @param publishers (tuple(unicode)): tuple of publishers (empty list for all, list of groups or list of jids) | |
763 # @param rsm (dict): TODO | |
764 # @return: dict{unicode: list[dict]) | |
765 # key: publisher's jid | |
766 # value: list of microblog data (dict) | |
767 # """ | |
768 # profile = session_iface.ISATSession(self.session).profile | |
769 # if rsm is None: | |
770 # rsm = {'max_': unicode(C.RSM_MAX_ITEMS)} | |
771 # d = self.asyncBridgeCall("getMassiveGroupBlogs", publishers_type, publishers, rsm, profile) | |
772 # self.sat_host.bridge.massiveSubscribeGroupBlogs(publishers_type, publishers, profile) | |
773 # return d | |
774 | |
775 # def jsonrpc_getMblogComments(self, service, node, rsm=None): | |
776 # """Get all comments of given node | |
777 # @param service: jid of the service hosting the node | |
778 # @param node: comments node | |
779 # """ | |
780 # profile = session_iface.ISATSession(self.session).profile | |
781 # if rsm is None: | |
782 # rsm = {'max_': unicode(C.RSM_MAX_COMMENTS)} | |
783 # d = self.asyncBridgeCall("getGroupBlogComments", service, node, rsm, profile) | |
784 # return d | |
785 | |
786 def jsonrpc_getPresenceStatuses(self): | |
787 """Get Presence information for connected contacts""" | |
788 profile = session_iface.ISATSession(self.session).profile | |
789 return self.sat_host.bridgeCall("getPresenceStatuses", profile) | |
790 | |
791 def jsonrpc_historyGet(self, from_jid, to_jid, size, between, search=""): | |
792 """Return history for the from_jid/to_jid couple""" | |
793 sat_session = session_iface.ISATSession(self.session) | |
794 profile = sat_session.profile | |
795 sat_jid = sat_session.jid | |
796 if not sat_jid: | |
797 raise exceptions.InternalError("session jid should be set") | |
798 if ( | |
799 jid.JID(from_jid).userhost() != sat_jid.userhost() | |
800 and jid.JID(to_jid).userhost() != sat_jid.userhost() | |
801 ): | |
802 log.error( | |
803 u"Trying to get history from a different jid (given (browser): {}, real (backend): {}), maybe a hack attempt ?".format( | |
804 from_jid, sat_jid | |
805 ) | |
806 ) | |
807 return {} | |
808 d = self.asyncBridgeCall( | |
809 "historyGet", from_jid, to_jid, size, between, search, profile | |
810 ) | |
811 | |
812 def show(result_dbus): | |
813 result = [] | |
814 for line in result_dbus: | |
815 # XXX: we have to do this stupid thing because Python D-Bus use its own types instead of standard types | |
816 # and txJsonRPC doesn't accept D-Bus types, resulting in a empty query | |
817 uuid, timestamp, from_jid, to_jid, message, subject, mess_type, extra = ( | |
818 line | |
819 ) | |
820 result.append( | |
821 ( | |
822 unicode(uuid), | |
823 float(timestamp), | |
824 unicode(from_jid), | |
825 unicode(to_jid), | |
826 dict(message), | |
827 dict(subject), | |
828 unicode(mess_type), | |
829 dict(extra), | |
830 ) | |
831 ) | |
832 return result | |
833 | |
834 d.addCallback(show) | |
835 return d | |
836 | |
837 def jsonrpc_mucJoin(self, room_jid, nick): | |
838 """Join a Multi-User Chat room | |
839 | |
840 @param room_jid (unicode): room JID or empty string to generate a unique name | |
841 @param nick (unicode): user nick | |
842 """ | |
843 profile = session_iface.ISATSession(self.session).profile | |
844 d = self.asyncBridgeCall("joinMUC", room_jid, nick, {}, profile) | |
845 return d | |
846 | |
847 def jsonrpc_inviteMUC(self, contact_jid, room_jid): | |
848 """Invite a user to a Multi-User Chat room | |
849 | |
850 @param contact_jid (unicode): contact to invite | |
851 @param room_jid (unicode): room JID or empty string to generate a unique name | |
852 """ | |
853 profile = session_iface.ISATSession(self.session).profile | |
854 room_id = room_jid.split("@")[0] | |
855 service = room_jid.split("@")[1] | |
856 return self.sat_host.bridgeCall( | |
857 "inviteMUC", contact_jid, service, room_id, {}, profile | |
858 ) | |
859 | |
860 def jsonrpc_mucLeave(self, room_jid): | |
861 """Quit a Multi-User Chat room""" | |
862 profile = session_iface.ISATSession(self.session).profile | |
863 try: | |
864 room_jid = jid.JID(room_jid) | |
865 except: | |
866 log.warning("Invalid room jid") | |
867 return | |
868 return self.sat_host.bridgeCall("mucLeave", room_jid.userhost(), profile) | |
869 | |
870 def jsonrpc_mucGetRoomsJoined(self): | |
871 """Return list of room already joined by user""" | |
872 profile = session_iface.ISATSession(self.session).profile | |
873 return self.sat_host.bridgeCall("mucGetRoomsJoined", profile) | |
874 | |
875 def jsonrpc_mucGetDefaultService(self): | |
876 """@return: the default MUC""" | |
877 d = self.asyncBridgeCall("mucGetDefaultService") | |
878 return d | |
879 | |
880 def jsonrpc_launchTarotGame(self, other_players, room_jid=""): | |
881 """Create a room, invite the other players and start a Tarot game. | |
882 | |
883 @param other_players (list[unicode]): JIDs of the players to play with | |
884 @param room_jid (unicode): room JID or empty string to generate a unique name | |
885 """ | |
886 profile = session_iface.ISATSession(self.session).profile | |
887 return self.sat_host.bridgeCall( | |
888 "tarotGameLaunch", other_players, room_jid, profile | |
889 ) | |
890 | |
891 def jsonrpc_getTarotCardsPaths(self): | |
892 """Give the path of all the tarot cards""" | |
893 _join = os.path.join | |
894 _media_dir = _join(self.sat_host.media_dir, "") | |
895 return map( | |
896 lambda x: _join(C.MEDIA_DIR, x[len(_media_dir) :]), | |
897 glob.glob(_join(_media_dir, C.CARDS_DIR, "*_*.png")), | |
898 ) | |
899 | |
900 def jsonrpc_tarotGameReady(self, player, referee): | |
901 """Tell to the server that we are ready to start the game""" | |
902 profile = session_iface.ISATSession(self.session).profile | |
903 return self.sat_host.bridgeCall("tarotGameReady", player, referee, profile) | |
904 | |
905 def jsonrpc_tarotGamePlayCards(self, player_nick, referee, cards): | |
906 """Tell to the server the cards we want to put on the table""" | |
907 profile = session_iface.ISATSession(self.session).profile | |
908 return self.sat_host.bridgeCall( | |
909 "tarotGamePlayCards", player_nick, referee, cards, profile | |
910 ) | |
911 | |
912 def jsonrpc_launchRadioCollective(self, invited, room_jid=""): | |
913 """Create a room, invite people, and start a radio collective. | |
914 | |
915 @param invited (list[unicode]): JIDs of the contacts to play with | |
916 @param room_jid (unicode): room JID or empty string to generate a unique name | |
917 """ | |
918 profile = session_iface.ISATSession(self.session).profile | |
919 return self.sat_host.bridgeCall("radiocolLaunch", invited, room_jid, profile) | |
920 | |
921 def jsonrpc_getEntitiesData(self, jids, keys): | |
922 """Get cached data for several entities at once | |
923 | |
924 @param jids: list jids from who we wants data, or empty list for all jids in cache | |
925 @param keys: name of data we want (list) | |
926 @return: requested data""" | |
927 if not C.ALLOWED_ENTITY_DATA.issuperset(keys): | |
928 raise exceptions.PermissionError( | |
929 "Trying to access unallowed data (hack attempt ?)" | |
930 ) | |
931 profile = session_iface.ISATSession(self.session).profile | |
932 try: | |
933 return self.sat_host.bridgeCall("getEntitiesData", jids, keys, profile) | |
934 except Exception as e: | |
935 raise failure.Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(e))) | |
936 | |
937 def jsonrpc_getEntityData(self, jid, keys): | |
938 """Get cached data for an entity | |
939 | |
940 @param jid: jid of contact from who we want data | |
941 @param keys: name of data we want (list) | |
942 @return: requested data""" | |
943 if not C.ALLOWED_ENTITY_DATA.issuperset(keys): | |
944 raise exceptions.PermissionError( | |
945 "Trying to access unallowed data (hack attempt ?)" | |
946 ) | |
947 profile = session_iface.ISATSession(self.session).profile | |
948 try: | |
949 return self.sat_host.bridgeCall("getEntityData", jid, keys, profile) | |
950 except Exception as e: | |
951 raise failure.Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(e))) | |
952 | |
953 def jsonrpc_getCard(self, jid_): | |
954 """Get VCard for entiry | |
955 @param jid_: jid of contact from who we want data | |
956 @return: id to retrieve the profile""" | |
957 profile = session_iface.ISATSession(self.session).profile | |
958 return self.sat_host.bridgeCall("getCard", jid_, profile) | |
959 | |
960 @defer.inlineCallbacks | |
961 def jsonrpc_avatarGet(self, entity, cache_only, hash_only): | |
962 session_data = session_iface.ISATSession(self.session) | |
963 profile = session_data.profile | |
964 # profile_uuid = session_data.uuid | |
965 avatar = yield self.asyncBridgeCall( | |
966 "avatarGet", entity, cache_only, hash_only, profile | |
967 ) | |
968 if hash_only: | |
969 defer.returnValue(avatar) | |
970 else: | |
971 filename = os.path.basename(avatar) | |
972 avatar_url = os.path.join(session_data.cache_dir, filename) | |
973 defer.returnValue(avatar_url) | |
974 | |
975 def jsonrpc_getAccountDialogUI(self): | |
976 """Get the dialog for managing user account | |
977 @return: XML string of the XMLUI""" | |
978 profile = session_iface.ISATSession(self.session).profile | |
979 return self.sat_host.bridgeCall("getAccountDialogUI", profile) | |
980 | |
981 def jsonrpc_getParamsUI(self): | |
982 """Return the parameters XML for profile""" | |
983 profile = session_iface.ISATSession(self.session).profile | |
984 return self.asyncBridgeCall("getParamsUI", C.SECURITY_LIMIT, C.APP_NAME, profile) | |
985 | |
986 def jsonrpc_asyncGetParamA(self, param, category, attribute="value"): | |
987 """Return the parameter value for profile""" | |
988 profile = session_iface.ISATSession(self.session).profile | |
989 if category == "Connection": | |
990 # we need to manage the followings params here, else SECURITY_LIMIT would block them | |
991 if param == "JabberID": | |
992 return self.asyncBridgeCall( | |
993 "asyncGetParamA", param, category, attribute, profile_key=profile | |
994 ) | |
995 elif param == "autoconnect": | |
996 return defer.succeed(C.BOOL_TRUE) | |
997 d = self.asyncBridgeCall( | |
998 "asyncGetParamA", | |
999 param, | |
1000 category, | |
1001 attribute, | |
1002 C.SECURITY_LIMIT, | |
1003 profile_key=profile, | |
1004 ) | |
1005 return d | |
1006 | |
1007 def jsonrpc_setParam(self, name, value, category): | |
1008 profile = session_iface.ISATSession(self.session).profile | |
1009 return self.sat_host.bridgeCall( | |
1010 "setParam", name, value, category, C.SECURITY_LIMIT, profile | |
1011 ) | |
1012 | |
1013 def jsonrpc_launchAction(self, callback_id, data): | |
1014 # FIXME: any action can be launched, this can be a huge security issue if callback_id can be guessed | |
1015 # a security system with authorised callback_id must be implemented, similar to the one for authorised params | |
1016 profile = session_iface.ISATSession(self.session).profile | |
1017 d = self.asyncBridgeCall("launchAction", callback_id, data, profile) | |
1018 return d | |
1019 | |
1020 def jsonrpc_chatStateComposing(self, to_jid_s): | |
1021 """Call the method to process a "composing" state. | |
1022 @param to_jid_s: contact the user is composing to | |
1023 """ | |
1024 profile = session_iface.ISATSession(self.session).profile | |
1025 return self.sat_host.bridgeCall("chatStateComposing", to_jid_s, profile) | |
1026 | |
1027 def jsonrpc_getNewAccountDomain(self): | |
1028 """@return: the domain for new account creation""" | |
1029 d = self.asyncBridgeCall("getNewAccountDomain") | |
1030 return d | |
1031 | |
1032 def jsonrpc_syntaxConvert( | |
1033 self, text, syntax_from=C.SYNTAX_XHTML, syntax_to=C.SYNTAX_CURRENT | |
1034 ): | |
1035 """ Convert a text between two syntaxes | |
1036 @param text: text to convert | |
1037 @param syntax_from: source syntax (e.g. "markdown") | |
1038 @param syntax_to: dest syntax (e.g.: "XHTML") | |
1039 @param safe: clean resulting XHTML to avoid malicious code if True (forced here) | |
1040 @return: converted text """ | |
1041 profile = session_iface.ISATSession(self.session).profile | |
1042 return self.sat_host.bridgeCall( | |
1043 "syntaxConvert", text, syntax_from, syntax_to, True, profile | |
1044 ) | |
1045 | |
1046 def jsonrpc_getLastResource(self, jid_s): | |
1047 """Get the last active resource of that contact.""" | |
1048 profile = session_iface.ISATSession(self.session).profile | |
1049 return self.sat_host.bridgeCall("getLastResource", jid_s, profile) | |
1050 | |
1051 def jsonrpc_getFeatures(self): | |
1052 """Return the available features in the backend for profile""" | |
1053 profile = session_iface.ISATSession(self.session).profile | |
1054 return self.sat_host.bridgeCall("getFeatures", profile) | |
1055 | |
1056 def jsonrpc_skipOTR(self): | |
1057 """Tell the backend to leave OTR handling to Libervia.""" | |
1058 profile = session_iface.ISATSession(self.session).profile | |
1059 return self.sat_host.bridgeCall("skipOTR", profile) | |
1060 | |
1061 def jsonrpc_namespacesGet(self): | |
1062 return self.sat_host.bridgeCall("namespacesGet") | |
1063 | |
1064 | |
1065 class WaitingRequests(dict): | |
1066 def setRequest(self, request, profile, register_with_ext_jid=False): | |
1067 """Add the given profile to the waiting list. | |
1068 | |
1069 @param request (server.Request): the connection request | |
1070 @param profile (str): %(doc_profile)s | |
1071 @param register_with_ext_jid (bool): True if we will try to register the profile with an external XMPP account credentials | |
1072 """ | |
1073 dc = reactor.callLater(BRIDGE_TIMEOUT, self.purgeRequest, profile) | |
1074 self[profile] = (request, dc, register_with_ext_jid) | |
1075 | |
1076 def purgeRequest(self, profile): | |
1077 """Remove the given profile from the waiting list. | |
1078 | |
1079 @param profile (str): %(doc_profile)s | |
1080 """ | |
1081 try: | |
1082 dc = self[profile][1] | |
1083 except KeyError: | |
1084 return | |
1085 if dc.active(): | |
1086 dc.cancel() | |
1087 del self[profile] | |
1088 | |
1089 def getRequest(self, profile): | |
1090 """Get the waiting request for the given profile. | |
1091 | |
1092 @param profile (str): %(doc_profile)s | |
1093 @return: the waiting request or None | |
1094 """ | |
1095 return self[profile][0] if profile in self else None | |
1096 | |
1097 def getRegisterWithExtJid(self, profile): | |
1098 """Get the value of the register_with_ext_jid parameter. | |
1099 | |
1100 @param profile (str): %(doc_profile)s | |
1101 @return: bool or None | |
1102 """ | |
1103 return self[profile][2] if profile in self else None | |
1104 | |
1105 | |
1106 class Register(JSONRPCMethodManager): | |
1107 """This class manage the registration procedure with SàT | |
1108 It provide an api for the browser, check password and setup the web server""" | |
1109 | |
1110 def __init__(self, sat_host): | |
1111 JSONRPCMethodManager.__init__(self, sat_host) | |
1112 self.profiles_waiting = {} | |
1113 self.request = None | |
1114 | |
1115 def render(self, request): | |
1116 """ | |
1117 Render method with some hacks: | |
1118 - if login is requested, try to login with form data | |
1119 - except login, every method is jsonrpc | |
1120 - user doesn't need to be authentified for explicitely listed methods, but must be for all others | |
1121 """ | |
1122 if request.postpath == ["login"]: | |
1123 return self.loginOrRegister(request) | |
1124 _session = request.getSession() | |
1125 parsed = jsonrpclib.loads(request.content.read()) | |
1126 method = parsed.get("method") # pylint: disable=E1103 | |
1127 if method not in ["getSessionMetadata", "registerParams", "menusGet"]: | |
1128 # if we don't call these methods, we need to be identified | |
1129 profile = session_iface.ISATSession(_session).profile | |
1130 if not profile: | |
1131 # user is not identified, we return a jsonrpc fault | |
1132 fault = jsonrpclib.Fault( | |
1133 C.ERRNUM_LIBERVIA, C.NOT_ALLOWED | |
1134 ) # FIXME: define some standard error codes for libervia | |
1135 return jsonrpc.JSONRPC._cbRender( | |
1136 self, fault, request, parsed.get("id"), parsed.get("jsonrpc") | |
1137 ) # pylint: disable=E1103 | |
1138 self.request = request | |
1139 return jsonrpc.JSONRPC.render(self, request) | |
1140 | |
1141 def loginOrRegister(self, request): | |
1142 """This method is called with the POST information from the registering form. | |
1143 | |
1144 @param request: request of the register form | |
1145 @return: a constant indicating the state: | |
1146 - C.BAD_REQUEST: something is wrong in the request (bad arguments) | |
1147 - a return value from self._loginAccount or self._registerNewAccount | |
1148 """ | |
1149 try: | |
1150 submit_type = request.args["submit_type"][0] | |
1151 except KeyError: | |
1152 return C.BAD_REQUEST | |
1153 | |
1154 if submit_type == "register": | |
1155 self._registerNewAccount(request) | |
1156 return server.NOT_DONE_YET | |
1157 elif submit_type == "login": | |
1158 self._loginAccount(request) | |
1159 return server.NOT_DONE_YET | |
1160 return Exception("Unknown submit type") | |
1161 | |
1162 @defer.inlineCallbacks | |
1163 def _registerNewAccount(self, request): | |
1164 try: | |
1165 login = request.args["register_login"][0] | |
1166 password = request.args["register_password"][0] | |
1167 email = request.args["email"][0] | |
1168 except KeyError: | |
1169 request.write(C.BAD_REQUEST) | |
1170 request.finish() | |
1171 return | |
1172 status = yield self.sat_host.registerNewAccount(request, login, password, email) | |
1173 request.write(status) | |
1174 request.finish() | |
1175 | |
1176 @defer.inlineCallbacks | |
1177 def _loginAccount(self, request): | |
1178 """Try to authenticate the user with the request information. | |
1179 | |
1180 will write to request a constant indicating the state: | |
1181 - C.PROFILE_LOGGED: profile is connected | |
1182 - C.PROFILE_LOGGED_EXT_JID: profile is connected and an external jid has been used | |
1183 - C.SESSION_ACTIVE: session was already active | |
1184 - C.BAD_REQUEST: something is wrong in the request (bad arguments) | |
1185 - C.PROFILE_AUTH_ERROR: either the profile (login) or the profile password is wrong | |
1186 - C.XMPP_AUTH_ERROR: the profile is authenticated but the XMPP password is wrong | |
1187 - C.ALREADY_WAITING: a request has already been submitted for this profil, C.PROFILE_LOGGED_EXT_JID)e | |
1188 - C.NOT_CONNECTED: connection has not been established | |
1189 the request will then be finished | |
1190 @param request: request of the register form | |
1191 """ | |
1192 try: | |
1193 login = request.args["login"][0] | |
1194 password = request.args["login_password"][0] | |
1195 except KeyError: | |
1196 request.write(C.BAD_REQUEST) | |
1197 request.finish() | |
1198 return | |
1199 | |
1200 assert login | |
1201 | |
1202 try: | |
1203 status = yield self.sat_host.connect(request, login, password) | |
1204 except ( | |
1205 exceptions.DataError, | |
1206 exceptions.ProfileUnknownError, | |
1207 exceptions.PermissionError, | |
1208 ): | |
1209 request.write(C.PROFILE_AUTH_ERROR) | |
1210 request.finish() | |
1211 return | |
1212 except exceptions.NotReady: | |
1213 request.write(C.ALREADY_WAITING) | |
1214 request.finish() | |
1215 return | |
1216 except exceptions.TimeOutError: | |
1217 request.write(C.NO_REPLY) | |
1218 request.finish() | |
1219 return | |
1220 except exceptions.InternalError as e: | |
1221 request.write(e.message) | |
1222 request.finish() | |
1223 return | |
1224 except exceptions.ConflictError: | |
1225 request.write(C.SESSION_ACTIVE) | |
1226 request.finish() | |
1227 return | |
1228 except ValueError as e: | |
1229 if e.message in (C.PROFILE_AUTH_ERROR, C.XMPP_AUTH_ERROR): | |
1230 request.write(e.message) | |
1231 request.finish() | |
1232 return | |
1233 else: | |
1234 raise e | |
1235 | |
1236 assert status | |
1237 request.write(status) | |
1238 request.finish() | |
1239 | |
1240 def jsonrpc_isConnected(self): | |
1241 _session = self.request.getSession() | |
1242 profile = session_iface.ISATSession(_session).profile | |
1243 return self.sat_host.bridgeCall("isConnected", profile) | |
1244 | |
1245 def jsonrpc_connect(self): | |
1246 _session = self.request.getSession() | |
1247 profile = session_iface.ISATSession(_session).profile | |
1248 if self.waiting_profiles.getRequest(profile): | |
1249 raise jsonrpclib.Fault( | |
1250 1, C.ALREADY_WAITING | |
1251 ) # FIXME: define some standard error codes for libervia | |
1252 self.waiting_profiles.setRequest(self.request, profile) | |
1253 self.sat_host.bridgeCall("connect", profile) | |
1254 return server.NOT_DONE_YET | |
1255 | |
1256 def jsonrpc_getSessionMetadata(self): | |
1257 """Return metadata useful on session start | |
1258 | |
1259 @return (dict): metadata which can have the following keys: | |
1260 "plugged" (bool): True if a profile is already plugged | |
1261 "warning" (unicode): a security warning message if plugged is False and if it make sense | |
1262 this key may not be present | |
1263 "allow_registration" (bool): True if registration is allowed | |
1264 this key is only present if profile is unplugged | |
1265 @return: a couple (registered, message) with: | |
1266 - registered: | |
1267 - message: | |
1268 """ | |
1269 metadata = {} | |
1270 _session = self.request.getSession() | |
1271 profile = session_iface.ISATSession(_session).profile | |
1272 if profile: | |
1273 metadata["plugged"] = True | |
1274 else: | |
1275 metadata["plugged"] = False | |
1276 metadata["warning"] = self._getSecurityWarning() | |
1277 metadata["allow_registration"] = self.sat_host.options["allow_registration"] | |
1278 return metadata | |
1279 | |
1280 def jsonrpc_registerParams(self): | |
1281 """Register the frontend specific parameters""" | |
1282 # params = """<params><individual>...</category></individual>""" | |
1283 # self.sat_host.bridge.paramsRegisterApp(params, C.SECURITY_LIMIT, C.APP_NAME) | |
1284 | |
1285 def jsonrpc_menusGet(self): | |
1286 """Return the parameters XML for profile""" | |
1287 # XXX: we put this method in Register because we get menus before being logged | |
1288 return self.sat_host.bridgeCall("menusGet", "", C.SECURITY_LIMIT) | |
1289 | |
1290 def _getSecurityWarning(self): | |
1291 """@return: a security warning message, or None if the connection is secure""" | |
1292 if ( | |
1293 self.request.URLPath().scheme == "https" | |
1294 or not self.sat_host.options["security_warning"] | |
1295 ): | |
1296 return None | |
1297 text = ( | |
1298 "<p>" | |
1299 + D_("You are about to connect to an unsecure service.") | |
1300 + "</p><p> </p><p>" | |
1301 ) | |
1302 | |
1303 if self.sat_host.options["connection_type"] == "both": | |
1304 new_port = ( | |
1305 (":%s" % self.sat_host.options["port_https_ext"]) | |
1306 if self.sat_host.options["port_https_ext"] != HTTPS_PORT | |
1307 else "" | |
1308 ) | |
1309 url = "https://%s" % self.request.URLPath().netloc.replace( | |
1310 ":%s" % self.sat_host.options["port"], new_port | |
1311 ) | |
1312 text += D_( | |
1313 "Please read our %(faq_prefix)ssecurity notice%(faq_suffix)s regarding HTTPS" | |
1314 ) % { | |
1315 "faq_prefix": '<a href="http://salut-a-toi.org/faq.html#https" target="#">', | |
1316 "faq_suffix": "</a>", | |
1317 } | |
1318 text += "</p><p>" + D_("and use the secure version of this website:") | |
1319 text += '</p><p> </p><p align="center"><a href="%(url)s">%(url)s</a>' % { | |
1320 "url": url | |
1321 } | |
1322 else: | |
1323 text += D_("You should ask your administrator to turn on HTTPS.") | |
1324 | |
1325 return text + "</p><p> </p>" | |
1326 | |
1327 | |
1328 class SignalHandler(jsonrpc.JSONRPC): | |
1329 def __init__(self, sat_host): | |
1330 web_resource.Resource.__init__(self) | |
1331 self.register = None | |
1332 self.sat_host = sat_host | |
1333 self._last_service_prof_disconnect = time.time() | |
1334 self.signalDeferred = {} # dict of deferred (key: profile, value: Deferred) | |
1335 # which manages the long polling HTTP request with signals | |
1336 self.queue = {} | |
1337 | |
1338 def plugRegister(self, register): | |
1339 self.register = register | |
1340 | |
1341 def jsonrpc_getSignals(self): | |
1342 """Keep the connection alive until a signal is received, then send it | |
1343 @return: (signal, *signal_args)""" | |
1344 _session = self.request.getSession() | |
1345 profile = session_iface.ISATSession(_session).profile | |
1346 if profile in self.queue: # if we have signals to send in queue | |
1347 if self.queue[profile]: | |
1348 return self.queue[profile].pop(0) | |
1349 else: | |
1350 # the queue is empty, we delete the profile from queue | |
1351 del self.queue[profile] | |
1352 _session.lock() # we don't want the session to expire as long as this connection is active | |
1353 | |
1354 def unlock(signal, profile): | |
1355 _session.unlock() | |
1356 try: | |
1357 source_defer = self.signalDeferred[profile] | |
1358 if source_defer.called and source_defer.result[0] == "disconnected": | |
1359 log.info(u"[%s] disconnected" % (profile,)) | |
1360 try: | |
1361 _session.expire() | |
1362 except KeyError: | |
1363 # FIXME: happen if session is ended using login page | |
1364 # when pyjamas page is also launched | |
1365 log.warning(u"session is already expired") | |
1366 except IndexError: | |
1367 log.error("Deferred result should be a tuple with fonction name first") | |
1368 | |
1369 self.signalDeferred[profile] = defer.Deferred() | |
1370 self.request.notifyFinish().addBoth(unlock, profile) | |
1371 return self.signalDeferred[profile] | |
1372 | |
1373 def getGenericCb(self, function_name): | |
1374 """Return a generic function which send all params to signalDeferred.callback | |
1375 function must have profile as last argument""" | |
1376 | |
1377 def genericCb(*args): | |
1378 profile = args[-1] | |
1379 if not profile in self.sat_host.prof_connected: | |
1380 return | |
1381 signal_data = (function_name, args[:-1]) | |
1382 try: | |
1383 signal_callback = self.signalDeferred[profile].callback | |
1384 except KeyError: | |
1385 self.queue.setdefault(profile, []).append(signal_data) | |
1386 else: | |
1387 signal_callback(signal_data) | |
1388 del self.signalDeferred[profile] | |
1389 | |
1390 return genericCb | |
1391 | |
1392 def actionNewHandler(self, action_data, action_id, security_limit, profile): | |
1393 """actionNew handler | |
1394 | |
1395 XXX: We need need a dedicated handler has actionNew use a security_limit which must be managed | |
1396 @param action_data(dict): see bridge documentation | |
1397 @param action_id(unicode): identitifer of the action | |
1398 @param security_limit(int): %(doc_security_limit)s | |
1399 @param profile(unicode): %(doc_profile)s | |
1400 """ | |
1401 if not profile in self.sat_host.prof_connected: | |
1402 return | |
1403 # FIXME: manage security limit in a dedicated method | |
1404 # raise an exception if it's not OK | |
1405 # and read value in sat.conf | |
1406 if security_limit >= C.SECURITY_LIMIT: | |
1407 log.debug( | |
1408 u"Ignoring action {action_id}, blocked by security limit".format( | |
1409 action_id=action_id | |
1410 ) | |
1411 ) | |
1412 return | |
1413 signal_data = ("actionNew", (action_data, action_id, security_limit)) | |
1414 try: | |
1415 signal_callback = self.signalDeferred[profile].callback | |
1416 except KeyError: | |
1417 self.queue.setdefault(profile, []).append(signal_data) | |
1418 else: | |
1419 signal_callback(signal_data) | |
1420 del self.signalDeferred[profile] | |
1421 | |
1422 def connected(self, profile, jid_s): | |
1423 """Connection is done. | |
1424 | |
1425 @param profile (unicode): %(doc_profile)s | |
1426 @param jid_s (unicode): the JID that we were assigned by the server, as the resource might differ from the JID we asked for. | |
1427 """ | |
1428 # FIXME: _logged should not be called from here, check this code | |
1429 # FIXME: check if needed to connect with external jid | |
1430 # jid_s is handled in QuickApp.connectionHandler already | |
1431 # assert self.register # register must be plugged | |
1432 # request = self.sat_host.waiting_profiles.getRequest(profile) | |
1433 # if request: | |
1434 # self.sat_host._logged(profile, request) | |
1435 | |
1436 def disconnected(self, profile): | |
1437 if profile == C.SERVICE_PROFILE: | |
1438 # if service profile has been disconnected, we try to reconnect it | |
1439 # if we can't we show error message | |
1440 # and if we have 2 disconnection in a short time, we don't try to reconnect | |
1441 # and display an error message | |
1442 disconnect_delta = time.time() - self._last_service_prof_disconnect | |
1443 if disconnect_delta < 15: | |
1444 log.error( | |
1445 _( | |
1446 u"Service profile disconnected twice in a short time, please check connection" | |
1447 ) | |
1448 ) | |
1449 else: | |
1450 log.info( | |
1451 _( | |
1452 u"Service profile has been disconnected, but we need it! Reconnecting it..." | |
1453 ) | |
1454 ) | |
1455 d = self.sat_host.bridgeCall( | |
1456 "connect", profile, self.sat_host.options["passphrase"], {} | |
1457 ) | |
1458 d.addErrback( | |
1459 lambda failure_: log.error( | |
1460 _( | |
1461 u"Can't reconnect service profile, please check connection: {reason}" | |
1462 ).format(reason=failure_) | |
1463 ) | |
1464 ) | |
1465 self._last_service_prof_disconnect = time.time() | |
1466 return | |
1467 | |
1468 if not profile in self.sat_host.prof_connected: | |
1469 log.info( | |
1470 _( | |
1471 u"'disconnected' signal received for a not connected profile ({profile})" | |
1472 ).format(profile=profile) | |
1473 ) | |
1474 return | |
1475 self.sat_host.prof_connected.remove(profile) | |
1476 if profile in self.signalDeferred: | |
1477 self.signalDeferred[profile].callback(("disconnected",)) | |
1478 del self.signalDeferred[profile] | |
1479 else: | |
1480 if profile not in self.queue: | |
1481 self.queue[profile] = [] | |
1482 self.queue[profile].append(("disconnected",)) | |
1483 | |
1484 def render(self, request): | |
1485 """ | |
1486 Render method wich reject access if user is not identified | |
1487 """ | |
1488 _session = request.getSession() | |
1489 parsed = jsonrpclib.loads(request.content.read()) | |
1490 profile = session_iface.ISATSession(_session).profile | |
1491 if not profile: | |
1492 # user is not identified, we return a jsonrpc fault | |
1493 fault = jsonrpclib.Fault( | |
1494 C.ERRNUM_LIBERVIA, C.NOT_ALLOWED | |
1495 ) # FIXME: define some standard error codes for libervia | |
1496 return jsonrpc.JSONRPC._cbRender( | |
1497 self, fault, request, parsed.get("id"), parsed.get("jsonrpc") | |
1498 ) # pylint: disable=E1103 | |
1499 self.request = request | |
1500 return jsonrpc.JSONRPC.render(self, request) | |
1501 | |
1502 | |
1503 class UploadManager(web_resource.Resource): | |
1504 """This class manage the upload of a file | |
1505 It redirect the stream to SàT core backend""" | |
1506 | |
1507 isLeaf = True | |
1508 NAME = "path" # name use by the FileUpload | |
1509 | |
1510 def __init__(self, sat_host): | |
1511 self.sat_host = sat_host | |
1512 self.upload_dir = tempfile.mkdtemp() | |
1513 self.sat_host.addCleanup(shutil.rmtree, self.upload_dir) | |
1514 | |
1515 def getTmpDir(self): | |
1516 return self.upload_dir | |
1517 | |
1518 def _getFileName(self, request): | |
1519 """Generate unique filename for a file""" | |
1520 raise NotImplementedError | |
1521 | |
1522 def _fileWritten(self, request, filepath): | |
1523 """Called once the file is actually written on disk | |
1524 @param request: HTTP request object | |
1525 @param filepath: full filepath on the server | |
1526 @return: a tuple with the name of the async bridge method | |
1527 to be called followed by its arguments. | |
1528 """ | |
1529 raise NotImplementedError | |
1530 | |
1531 def render(self, request): | |
1532 """ | |
1533 Render method with some hacks: | |
1534 - if login is requested, try to login with form data | |
1535 - except login, every method is jsonrpc | |
1536 - user doesn't need to be authentified for getSessionMetadata, but must be for all other methods | |
1537 """ | |
1538 filename = self._getFileName(request) | |
1539 filepath = os.path.join(self.upload_dir, filename) | |
1540 # FIXME: the uploaded file is fully loaded in memory at form parsing time so far | |
1541 # (see twisted.web.http.Request.requestReceived). A custom requestReceived should | |
1542 # be written in the futur. In addition, it is not yet possible to get progression informations | |
1543 # (see http://twistedmatrix.com/trac/ticket/288) | |
1544 | |
1545 with open(filepath, "w") as f: | |
1546 f.write(request.args[self.NAME][0]) | |
1547 | |
1548 def finish(d): | |
1549 error = isinstance(d, Exception) or isinstance(d, failure.Failure) | |
1550 request.write(C.UPLOAD_KO if error else C.UPLOAD_OK) | |
1551 # TODO: would be great to re-use the original Exception class and message | |
1552 # but it is lost in the middle of the backtrace and encapsulated within | |
1553 # a DBusException instance --> extract the data from the backtrace? | |
1554 request.finish() | |
1555 | |
1556 d = JSONRPCMethodManager(self.sat_host).asyncBridgeCall( | |
1557 *self._fileWritten(request, filepath) | |
1558 ) | |
1559 d.addCallbacks(lambda d: finish(d), lambda failure: finish(failure)) | |
1560 return server.NOT_DONE_YET | |
1561 | |
1562 | |
1563 class UploadManagerRadioCol(UploadManager): | |
1564 NAME = "song" | |
1565 | |
1566 def _getFileName(self, request): | |
1567 extension = os.path.splitext(request.args["filename"][0])[1] | |
1568 return "%s%s" % ( | |
1569 str(uuid.uuid4()), | |
1570 extension, | |
1571 ) # XXX: chromium doesn't seem to play song without the .ogg extension, even with audio/ogg mime-type | |
1572 | |
1573 def _fileWritten(self, request, filepath): | |
1574 """Called once the file is actually written on disk | |
1575 @param request: HTTP request object | |
1576 @param filepath: full filepath on the server | |
1577 @return: a tuple with the name of the async bridge method | |
1578 to be called followed by its arguments. | |
1579 """ | |
1580 profile = session_iface.ISATSession(request.getSession()).profile | |
1581 return ("radiocolSongAdded", request.args["referee"][0], filepath, profile) | |
1582 | |
1583 | |
1584 class UploadManagerAvatar(UploadManager): | |
1585 NAME = "avatar_path" | |
1586 | |
1587 def _getFileName(self, request): | |
1588 return str(uuid.uuid4()) | |
1589 | |
1590 def _fileWritten(self, request, filepath): | |
1591 """Called once the file is actually written on disk | |
1592 @param request: HTTP request object | |
1593 @param filepath: full filepath on the server | |
1594 @return: a tuple with the name of the async bridge method | |
1595 to be called followed by its arguments. | |
1596 """ | |
1597 profile = session_iface.ISATSession(request.getSession()).profile | |
1598 return ("setAvatar", filepath, profile) | |
1599 | |
1600 | |
1601 class Libervia(service.Service): | |
1602 debug = defer.Deferred.debug # True if twistd/Libervia is launched in debug mode | |
1603 | |
1604 def __init__(self, options): | |
1605 self.options = options | |
1606 self.initialised = defer.Deferred() | |
1607 self.waiting_profiles = WaitingRequests() # FIXME: should be removed | |
1608 | |
1609 if self.options["base_url_ext"]: | |
1610 self.base_url_ext = self.options.pop("base_url_ext") | |
1611 if self.base_url_ext[-1] != "/": | |
1612 self.base_url_ext += "/" | |
1613 self.base_url_ext_data = urlparse.urlsplit(self.base_url_ext) | |
1614 else: | |
1615 self.base_url_ext = None | |
1616 # we split empty string anyway so we can do things like | |
1617 # scheme = self.base_url_ext_data.scheme or 'https' | |
1618 self.base_url_ext_data = urlparse.urlsplit("") | |
1619 | |
1620 if not self.options["port_https_ext"]: | |
1621 self.options["port_https_ext"] = self.options["port_https"] | |
1622 if self.options["data_dir"] == DATA_DIR_DEFAULT: | |
1623 coerceDataDir( | |
1624 self.options["data_dir"] | |
1625 ) # this is not done when using the default value | |
1626 | |
1627 self.html_dir = os.path.join(self.options["data_dir"], C.HTML_DIR) | |
1628 self.themes_dir = os.path.join(self.options["data_dir"], C.THEMES_DIR) | |
1629 | |
1630 self._cleanup = [] | |
1631 | |
1632 self.signal_handler = SignalHandler(self) | |
1633 self.sessions = {} # key = session value = user | |
1634 self.prof_connected = set() # Profiles connected | |
1635 self.ns_map = {} # map of short name to namespaces | |
1636 | |
1637 ## bridge ## | |
1638 try: | |
1639 self.bridge = Bridge() | |
1640 except BridgeExceptionNoService: | |
1641 print(u"Can't connect to SàT backend, are you sure it's launched ?") | |
1642 sys.exit(1) | |
1643 self.bridge.bridgeConnect(callback=self._bridgeCb, errback=self._bridgeEb) | |
1644 | |
1645 def _namespacesGetCb(self, ns_map): | |
1646 self.ns_map = ns_map | |
1647 | |
1648 def _namespacesGetEb(self, failure_): | |
1649 log.error(_(u"Can't get namespaces map: {msg}").format(msg=failure_)) | |
1650 | |
1651 def backendReady(self, dummy): | |
1652 self.root = root = LiberviaRootResource(self.html_dir) | |
1653 _register = Register(self) | |
1654 _upload_radiocol = UploadManagerRadioCol(self) | |
1655 _upload_avatar = UploadManagerAvatar(self) | |
1656 d = self.bridgeCall("namespacesGet") | |
1657 d.addCallback(self._namespacesGetCb) | |
1658 d.addErrback(self._namespacesGetEb) | |
1659 self.signal_handler.plugRegister(_register) | |
1660 self.bridge.register_signal("connected", self.signal_handler.connected) | |
1661 self.bridge.register_signal("disconnected", self.signal_handler.disconnected) | |
1662 # core | |
1663 for signal_name in [ | |
1664 "presenceUpdate", | |
1665 "messageNew", | |
1666 "subscribe", | |
1667 "contactDeleted", | |
1668 "newContact", | |
1669 "entityDataUpdated", | |
1670 "paramUpdate", | |
1671 ]: | |
1672 self.bridge.register_signal( | |
1673 signal_name, self.signal_handler.getGenericCb(signal_name) | |
1674 ) | |
1675 # XXX: actionNew is handled separately because the handler must manage security_limit | |
1676 self.bridge.register_signal("actionNew", self.signal_handler.actionNewHandler) | |
1677 # plugins | |
1678 for signal_name in [ | |
1679 "psEvent", | |
1680 "mucRoomJoined", | |
1681 "tarotGameStarted", | |
1682 "tarotGameNew", | |
1683 "tarotGameChooseContrat", | |
1684 "tarotGameShowCards", | |
1685 "tarotGameInvalidCards", | |
1686 "tarotGameCardsPlayed", | |
1687 "tarotGameYourTurn", | |
1688 "tarotGameScore", | |
1689 "tarotGamePlayers", | |
1690 "radiocolStarted", | |
1691 "radiocolPreload", | |
1692 "radiocolPlay", | |
1693 "radiocolNoUpload", | |
1694 "radiocolUploadOk", | |
1695 "radiocolSongRejected", | |
1696 "radiocolPlayers", | |
1697 "mucRoomLeft", | |
1698 "mucRoomUserChangedNick", | |
1699 "chatStateReceived", | |
1700 ]: | |
1701 self.bridge.register_signal( | |
1702 signal_name, self.signal_handler.getGenericCb(signal_name), "plugin" | |
1703 ) | |
1704 self.media_dir = self.bridge.getConfig("", "media_dir") | |
1705 self.local_dir = self.bridge.getConfig("", "local_dir") | |
1706 self.cache_root_dir = os.path.join(self.local_dir, C.CACHE_DIR) | |
1707 | |
1708 # JSON APIs | |
1709 self.putChild("json_signal_api", self.signal_handler) | |
1710 self.putChild("json_api", MethodHandler(self)) | |
1711 self.putChild("register_api", _register) | |
1712 | |
1713 # files upload | |
1714 self.putChild("upload_radiocol", _upload_radiocol) | |
1715 self.putChild("upload_avatar", _upload_avatar) | |
1716 | |
1717 # static pages | |
1718 self.putChild("blog_legacy", MicroBlog(self)) | |
1719 self.putChild(C.THEMES_URL, ProtectedFile(self.themes_dir)) | |
1720 | |
1721 # websocket | |
1722 if self.options["connection_type"] in ("https", "both"): | |
1723 wss = websockets.LiberviaPageWSProtocol.getResource(self, secure=True) | |
1724 self.putChild("wss", wss) | |
1725 if self.options["connection_type"] in ("http", "both"): | |
1726 ws = websockets.LiberviaPageWSProtocol.getResource(self, secure=False) | |
1727 self.putChild("ws", ws) | |
1728 | |
1729 # Libervia pages | |
1730 LiberviaPage.importPages(self) | |
1731 LiberviaPage.setMenu(self.options["menu_json"]) | |
1732 ## following signal is needed for cache handling in Libervia pages | |
1733 self.bridge.register_signal( | |
1734 "psEventRaw", partial(LiberviaPage.onNodeEvent, self), "plugin" | |
1735 ) | |
1736 self.bridge.register_signal( | |
1737 "messageNew", partial(LiberviaPage.onSignal, self, "messageNew") | |
1738 ) | |
1739 | |
1740 # Progress handling | |
1741 self.bridge.register_signal( | |
1742 "progressStarted", partial(ProgressHandler._signal, "started") | |
1743 ) | |
1744 self.bridge.register_signal( | |
1745 "progressFinished", partial(ProgressHandler._signal, "finished") | |
1746 ) | |
1747 self.bridge.register_signal( | |
1748 "progressError", partial(ProgressHandler._signal, "error") | |
1749 ) | |
1750 | |
1751 # media dirs | |
1752 # FIXME: get rid of dirname and "/" in C.XXX_DIR | |
1753 self.putChild(os.path.dirname(C.MEDIA_DIR), ProtectedFile(self.media_dir)) | |
1754 self.cache_resource = web_resource.NoResource() | |
1755 self.putChild(C.CACHE_DIR, self.cache_resource) | |
1756 | |
1757 # special | |
1758 self.putChild( | |
1759 "radiocol", | |
1760 ProtectedFile(_upload_radiocol.getTmpDir(), defaultType="audio/ogg"), | |
1761 ) # FIXME: We cheat for PoC because we know we are on the same host, so we use directly upload dir | |
1762 # pyjamas tests, redirected only for dev versions | |
1763 if self.version[-1] == "D": | |
1764 self.putChild("test", web_util.Redirect("/libervia_test.html")) | |
1765 | |
1766 # redirections | |
1767 root._initRedirections(self.options) | |
1768 | |
1769 server.Request.defaultContentType = "text/html; charset=utf-8" | |
1770 wrapped = web_resource.EncodingResourceWrapper( | |
1771 root, [server.GzipEncoderFactory()] | |
1772 ) | |
1773 self.site = server.Site(wrapped) | |
1774 self.site.sessionFactory = LiberviaSession | |
1775 self.renderer = template.Renderer(self) | |
1776 self.putChild("templates", ProtectedFile(self.renderer.base_dir)) | |
1777 | |
1778 def initEb(self, failure): | |
1779 log.error(_(u"Init error: {msg}").format(msg=failure)) | |
1780 reactor.stop() | |
1781 return failure | |
1782 | |
1783 def _bridgeCb(self): | |
1784 self.bridge.getReady( | |
1785 lambda: self.initialised.callback(None), | |
1786 lambda failure: self.initialised.errback(Exception(failure)), | |
1787 ) | |
1788 self.initialised.addCallback(self.backendReady) | |
1789 self.initialised.addErrback(self.initEb) | |
1790 | |
1791 def _bridgeEb(self, failure): | |
1792 log.error(u"Can't connect to bridge: {}".format(failure)) | |
1793 | |
1794 @property | |
1795 def version(self): | |
1796 """Return the short version of Libervia""" | |
1797 return C.APP_VERSION | |
1798 | |
1799 @property | |
1800 def full_version(self): | |
1801 """Return the full version of Libervia (with extra data when in development mode)""" | |
1802 version = self.version | |
1803 if version[-1] == "D": | |
1804 # we are in debug version, we add extra data | |
1805 try: | |
1806 return self._version_cache | |
1807 except AttributeError: | |
1808 self._version_cache = u"{} ({})".format( | |
1809 version, utils.getRepositoryData(libervia) | |
1810 ) | |
1811 return self._version_cache | |
1812 else: | |
1813 return version | |
1814 | |
1815 def bridgeCall(self, method_name, *args, **kwargs): | |
1816 """Call an asynchronous bridge method and return a deferred | |
1817 | |
1818 @param method_name: name of the method as a unicode | |
1819 @return: a deferred which trigger the result | |
1820 | |
1821 """ | |
1822 d = defer.Deferred() | |
1823 | |
1824 def _callback(*args): | |
1825 if not args: | |
1826 d.callback(None) | |
1827 else: | |
1828 if len(args) != 1: | |
1829 Exception("Multiple return arguments not supported") | |
1830 d.callback(args[0]) | |
1831 | |
1832 def _errback(result): | |
1833 d.errback( | |
1834 failure.Failure( | |
1835 jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, result.classname) | |
1836 ) | |
1837 ) | |
1838 | |
1839 kwargs["callback"] = _callback | |
1840 kwargs["errback"] = _errback | |
1841 getattr(self.bridge, method_name)(*args, **kwargs) | |
1842 return d | |
1843 | |
1844 @defer.inlineCallbacks | |
1845 def _logged(self, profile, request): | |
1846 """Set everything when a user just logged in | |
1847 | |
1848 @param profile | |
1849 @param request | |
1850 @return: a constant indicating the state: | |
1851 - C.PROFILE_LOGGED | |
1852 - C.PROFILE_LOGGED_EXT_JID | |
1853 @raise exceptions.ConflictError: session is already active | |
1854 """ | |
1855 register_with_ext_jid = self.waiting_profiles.getRegisterWithExtJid(profile) | |
1856 self.waiting_profiles.purgeRequest(profile) | |
1857 session = request.getSession() | |
1858 sat_session = session_iface.ISATSession(session) | |
1859 if sat_session.profile: | |
1860 log.error(_(u"/!\\ Session has already a profile, this should NEVER happen!")) | |
1861 raise failure.Failure(exceptions.ConflictError("Already active")) | |
1862 | |
1863 sat_session.profile = profile | |
1864 self.prof_connected.add(profile) | |
1865 cache_dir = os.path.join( | |
1866 self.cache_root_dir, u"profiles", regex.pathEscape(profile) | |
1867 ) | |
1868 # FIXME: would be better to have a global /cache URL which redirect to profile's cache directory, without uuid | |
1869 self.cache_resource.putChild(sat_session.uuid, ProtectedFile(cache_dir)) | |
1870 log.debug( | |
1871 _(u"profile cache resource added from {uuid} to {path}").format( | |
1872 uuid=sat_session.uuid, path=cache_dir | |
1873 ) | |
1874 ) | |
1875 | |
1876 def onExpire(): | |
1877 log.info(u"Session expired (profile={profile})".format(profile=profile)) | |
1878 self.cache_resource.delEntity(sat_session.uuid) | |
1879 log.debug( | |
1880 _(u"profile cache resource {uuid} deleted").format(uuid=sat_session.uuid) | |
1881 ) | |
1882 try: | |
1883 # We purge the queue | |
1884 del self.signal_handler.queue[profile] | |
1885 except KeyError: | |
1886 pass | |
1887 # and now we disconnect the profile | |
1888 self.bridgeCall("disconnect", profile) | |
1889 | |
1890 session.notifyOnExpire(onExpire) | |
1891 | |
1892 # FIXME: those session infos should be returned by connect or isConnected | |
1893 infos = yield self.bridgeCall("sessionInfosGet", profile) | |
1894 sat_session.jid = jid.JID(infos["jid"]) | |
1895 sat_session.backend_started = int(infos["started"]) | |
1896 | |
1897 state = C.PROFILE_LOGGED_EXT_JID if register_with_ext_jid else C.PROFILE_LOGGED | |
1898 defer.returnValue(state) | |
1899 | |
1900 @defer.inlineCallbacks | |
1901 def connect(self, request, login, password): | |
1902 """log user in | |
1903 | |
1904 If an other user was already logged, it will be unlogged first | |
1905 @param request(server.Request): request linked to the session | |
1906 @param login(unicode): user login | |
1907 can be profile name | |
1908 can be profile@[libervia_domain.ext] | |
1909 can be a jid (a new profile will be created with this jid if needed) | |
1910 @param password(unicode): user password | |
1911 @return (unicode, None): C.SESSION_ACTIVE: if session was aleady active else self._logged value | |
1912 @raise exceptions.DataError: invalid login | |
1913 @raise exceptions.ProfileUnknownError: this login doesn't exist | |
1914 @raise exceptions.PermissionError: a login is not accepted (e.g. empty password not allowed) | |
1915 @raise exceptions.NotReady: a profile connection is already waiting | |
1916 @raise exceptions.TimeoutError: didn't received and answer from Bridge | |
1917 @raise exceptions.InternalError: unknown error | |
1918 @raise ValueError(C.PROFILE_AUTH_ERROR): invalid login and/or password | |
1919 @raise ValueError(C.XMPP_AUTH_ERROR): invalid XMPP account password | |
1920 """ | |
1921 | |
1922 # XXX: all security checks must be done here, even if present in javascript | |
1923 if login.startswith("@"): | |
1924 raise failure.Failure(exceptions.DataError("No profile_key allowed")) | |
1925 | |
1926 if login.startswith("guest@@") and login.count("@") == 2: | |
1927 log.debug("logging a guest account") | |
1928 elif "@" in login: | |
1929 if login.count("@") != 1: | |
1930 raise failure.Failure( | |
1931 exceptions.DataError("Invalid login: {login}".format(login=login)) | |
1932 ) | |
1933 try: | |
1934 login_jid = jid.JID(login) | |
1935 except (RuntimeError, jid.InvalidFormat, AttributeError): | |
1936 raise failure.Failure(exceptions.DataError("No profile_key allowed")) | |
1937 | |
1938 # FIXME: should it be cached? | |
1939 new_account_domain = yield self.bridgeCall("getNewAccountDomain") | |
1940 | |
1941 if login_jid.host == new_account_domain: | |
1942 # redirect "user@libervia.org" to the "user" profile | |
1943 login = login_jid.user | |
1944 login_jid = None | |
1945 else: | |
1946 login_jid = None | |
1947 | |
1948 try: | |
1949 profile = yield self.bridgeCall("profileNameGet", login) | |
1950 except Exception: # XXX: ProfileUnknownError wouldn't work, it's encapsulated | |
1951 # FIXME: find a better way to handle bridge errors | |
1952 if ( | |
1953 login_jid is not None and login_jid.user | |
1954 ): # try to create a new sat profile using the XMPP credentials | |
1955 if not self.options["allow_registration"]: | |
1956 log.warning( | |
1957 u"Trying to register JID account while registration is not allowed" | |
1958 ) | |
1959 raise failure.Failure( | |
1960 exceptions.DataError( | |
1961 u"JID login while registration is not allowed" | |
1962 ) | |
1963 ) | |
1964 profile = login # FIXME: what if there is a resource? | |
1965 connect_method = "asyncConnectWithXMPPCredentials" | |
1966 register_with_ext_jid = True | |
1967 else: # non existing username | |
1968 raise failure.Failure(exceptions.ProfileUnknownError()) | |
1969 else: | |
1970 if profile != login or ( | |
1971 not password | |
1972 and profile | |
1973 not in self.options["empty_password_allowed_warning_dangerous_list"] | |
1974 ): | |
1975 # profiles with empty passwords are restricted to local frontends | |
1976 raise exceptions.PermissionError | |
1977 register_with_ext_jid = False | |
1978 | |
1979 connect_method = "connect" | |
1980 | |
1981 # we check if there is not already an active session | |
1982 sat_session = session_iface.ISATSession(request.getSession()) | |
1983 if sat_session.profile: | |
1984 # yes, there is | |
1985 if sat_session.profile != profile: | |
1986 # it's a different profile, we need to disconnect it | |
1987 log.warning( | |
1988 _( | |
1989 u"{new_profile} requested login, but {old_profile} was already connected, disconnecting {old_profile}" | |
1990 ).format(old_profile=sat_session.profile, new_profile=profile) | |
1991 ) | |
1992 self.purgeSession(request) | |
1993 | |
1994 if self.waiting_profiles.getRequest(profile): | |
1995 # FIXME: check if and when this can happen | |
1996 raise failure.Failure(exceptions.NotReady("Already waiting")) | |
1997 | |
1998 self.waiting_profiles.setRequest(request, profile, register_with_ext_jid) | |
1999 try: | |
2000 connected = yield self.bridgeCall(connect_method, profile, password) | |
2001 except Exception as failure_: | |
2002 fault = failure_.faultString | |
2003 self.waiting_profiles.purgeRequest(profile) | |
2004 if fault in ("PasswordError", "ProfileUnknownError"): | |
2005 log.info( | |
2006 u"Profile {profile} doesn't exist or the submitted password is wrong".format( | |
2007 profile=profile | |
2008 ) | |
2009 ) | |
2010 raise failure.Failure(ValueError(C.PROFILE_AUTH_ERROR)) | |
2011 elif fault == "SASLAuthError": | |
2012 log.info( | |
2013 u"The XMPP password of profile {profile} is wrong".format( | |
2014 profile=profile | |
2015 ) | |
2016 ) | |
2017 raise failure.Failure(ValueError(C.XMPP_AUTH_ERROR)) | |
2018 elif fault == "NoReply": | |
2019 log.info( | |
2020 _( | |
2021 "Did not receive a reply (the timeout expired or the connection is broken)" | |
2022 ) | |
2023 ) | |
2024 raise exceptions.TimeOutError | |
2025 else: | |
2026 log.error( | |
2027 u'Unmanaged fault string "{fault}" in errback for the connection of profile {profile}'.format( | |
2028 fault=fault, profile=profile | |
2029 ) | |
2030 ) | |
2031 raise failure.Failure(exceptions.InternalError(fault)) | |
2032 | |
2033 if connected: | |
2034 # profile is already connected in backend | |
2035 # do we have a corresponding session in Libervia? | |
2036 sat_session = session_iface.ISATSession(request.getSession()) | |
2037 if sat_session.profile: | |
2038 # yes, session is active | |
2039 if sat_session.profile != profile: | |
2040 # existing session should have been ended above | |
2041 # so this line should never be reached | |
2042 log.error( | |
2043 _( | |
2044 u"session profile [{session_profile}] differs from login profile [{profile}], this should not happen!" | |
2045 ).format(session_profile=sat_session.profile, profile=profile) | |
2046 ) | |
2047 raise exceptions.InternalError("profile mismatch") | |
2048 defer.returnValue(C.SESSION_ACTIVE) | |
2049 log.info( | |
2050 _( | |
2051 u"profile {profile} was already connected in backend".format( | |
2052 profile=profile | |
2053 ) | |
2054 ) | |
2055 ) | |
2056 # no, we have to create it | |
2057 | |
2058 state = yield self._logged(profile, request) | |
2059 defer.returnValue(state) | |
2060 | |
2061 def registerNewAccount(self, request, login, password, email): | |
2062 """Create a new account, or return error | |
2063 @param request(server.Request): request linked to the session | |
2064 @param login(unicode): new account requested login | |
2065 @param email(unicode): new account email | |
2066 @param password(unicode): new account password | |
2067 @return(unicode): a constant indicating the state: | |
2068 - C.BAD_REQUEST: something is wrong in the request (bad arguments) | |
2069 - C.INVALID_INPUT: one of the data is not valid | |
2070 - C.REGISTRATION_SUCCEED: new account has been successfully registered | |
2071 - C.ALREADY_EXISTS: the given profile already exists | |
2072 - C.INTERNAL_ERROR or any unmanaged fault string | |
2073 @raise PermissionError: registration is now allowed in server configuration | |
2074 """ | |
2075 if not self.options["allow_registration"]: | |
2076 log.warning( | |
2077 _(u"Registration received while it is not allowed, hack attempt?") | |
2078 ) | |
2079 raise failure.Failure( | |
2080 exceptions.PermissionError(u"Registration is not allowed on this server") | |
2081 ) | |
2082 | |
2083 if ( | |
2084 not re.match(C.REG_LOGIN_RE, login) | |
2085 or not re.match(C.REG_EMAIL_RE, email, re.IGNORECASE) | |
2086 or len(password) < C.PASSWORD_MIN_LENGTH | |
2087 ): | |
2088 return C.INVALID_INPUT | |
2089 | |
2090 def registered(result): | |
2091 return C.REGISTRATION_SUCCEED | |
2092 | |
2093 def registeringError(failure): | |
2094 status = failure.value.faultString | |
2095 if status == "ConflictError": | |
2096 return C.ALREADY_EXISTS | |
2097 elif status == "InternalError": | |
2098 return C.INTERNAL_ERROR | |
2099 else: | |
2100 log.error( | |
2101 _(u"Unknown registering error status: {status }").format( | |
2102 status=status | |
2103 ) | |
2104 ) | |
2105 return status | |
2106 | |
2107 d = self.bridgeCall("registerSatAccount", email, password, login) | |
2108 d.addCallback(registered) | |
2109 d.addErrback(registeringError) | |
2110 return d | |
2111 | |
2112 def addCleanup(self, callback, *args, **kwargs): | |
2113 """Add cleaning method to call when service is stopped | |
2114 | |
2115 cleaning method will be called in reverse order of they insertion | |
2116 @param callback: callable to call on service stop | |
2117 @param *args: list of arguments of the callback | |
2118 @param **kwargs: list of keyword arguments of the callback""" | |
2119 self._cleanup.insert(0, (callback, args, kwargs)) | |
2120 | |
2121 def startService(self): | |
2122 """Connect the profile for Libervia and start the HTTP(S) server(s)""" | |
2123 | |
2124 def eb(e): | |
2125 log.error(_(u"Connection failed: %s") % e) | |
2126 self.stop() | |
2127 | |
2128 def initOk(dummy): | |
2129 try: | |
2130 connected = self.bridge.isConnected(C.SERVICE_PROFILE) | |
2131 except Exception as e: | |
2132 # we don't want the traceback | |
2133 msg = [l for l in unicode(e).split("\n") if l][-1] | |
2134 log.error( | |
2135 u"Can't check service profile ({profile}), are you sure it exists ?\n{error}".format( | |
2136 profile=C.SERVICE_PROFILE, error=msg | |
2137 ) | |
2138 ) | |
2139 self.stop() | |
2140 return | |
2141 if not connected: | |
2142 self.bridge.connect( | |
2143 C.SERVICE_PROFILE, | |
2144 self.options["passphrase"], | |
2145 {}, | |
2146 callback=self._startService, | |
2147 errback=eb, | |
2148 ) | |
2149 else: | |
2150 self._startService() | |
2151 | |
2152 self.initialised.addCallback(initOk) | |
2153 | |
2154 ## URLs ## | |
2155 | |
2156 def putChild(self, path, resource): | |
2157 """Add a child to the root resource""" | |
2158 # FIXME: check that no information is leaked (c.f. https://twistedmatrix.com/documents/current/web/howto/using-twistedweb.html#request-encoders) | |
2159 self.root.putChild( | |
2160 path, | |
2161 web_resource.EncodingResourceWrapper(resource, [server.GzipEncoderFactory()]), | |
2162 ) | |
2163 | |
2164 def getExtBaseURLData(self, request): | |
2165 """Retrieve external base URL Data | |
2166 | |
2167 this method tried to retrieve the base URL found by external user | |
2168 It does by checking in this order: | |
2169 - base_url_ext option from configuration | |
2170 - proxy x-forwarder-host headers | |
2171 - URL of the request | |
2172 @return (urlparse.SplitResult): SplitResult instance with only scheme and netloc filled | |
2173 """ | |
2174 ext_data = self.base_url_ext_data | |
2175 url_path = request.URLPath() | |
2176 if not ext_data.scheme or not ext_data.netloc: | |
2177 # ext_data is not specified, we check headers | |
2178 if request.requestHeaders.hasHeader("x-forwarded-host"): | |
2179 # we are behing a proxy | |
2180 # we fill proxy_scheme and proxy_netloc value | |
2181 proxy_host = request.requestHeaders.getRawHeaders("x-forwarded-host")[0] | |
2182 try: | |
2183 proxy_server = request.requestHeaders.getRawHeaders( | |
2184 "x-forwarded-server" | |
2185 )[0] | |
2186 except TypeError: | |
2187 # no x-forwarded-server found, we use proxy_host | |
2188 proxy_netloc = proxy_host | |
2189 else: | |
2190 # if the proxy host has a port, we use it with server name | |
2191 proxy_port = urlparse.urlsplit(u"//{}".format(proxy_host)).port | |
2192 proxy_netloc = ( | |
2193 u"{}:{}".format(proxy_server, proxy_port) | |
2194 if proxy_port is not None | |
2195 else proxy_server | |
2196 ) | |
2197 proxy_netloc = proxy_netloc.decode("utf-8") | |
2198 try: | |
2199 proxy_scheme = request.requestHeaders.getRawHeaders( | |
2200 "x-forwarded-proto" | |
2201 )[0].decode("utf-8") | |
2202 except TypeError: | |
2203 proxy_scheme = None | |
2204 else: | |
2205 proxy_scheme, proxy_netloc = None, None | |
2206 else: | |
2207 proxy_scheme, proxy_netloc = None, None | |
2208 | |
2209 return urlparse.SplitResult( | |
2210 ext_data.scheme or proxy_scheme or url_path.scheme.decode("utf-8"), | |
2211 ext_data.netloc or proxy_netloc or url_path.netloc.decode("utf-8"), | |
2212 ext_data.path or u"/", | |
2213 "", | |
2214 "", | |
2215 ) | |
2216 | |
2217 def getExtBaseURL(self, request, path="", query="", fragment="", scheme=None): | |
2218 """Get external URL according to given elements | |
2219 | |
2220 external URL is the URL seen by external user | |
2221 @param path(unicode): same as for urlsplit.urlsplit | |
2222 path will be prefixed to follow found external URL if suitable | |
2223 @param params(unicode): same as for urlsplit.urlsplit | |
2224 @param query(unicode): same as for urlsplit.urlsplit | |
2225 @param fragment(unicode): same as for urlsplit.urlsplit | |
2226 @param scheme(unicode, None): if not None, will override scheme from base URL | |
2227 @return (unicode): external URL | |
2228 """ | |
2229 split_result = self.getExtBaseURLData(request) | |
2230 return urlparse.urlunsplit( | |
2231 ( | |
2232 split_result.scheme.decode("utf-8") if scheme is None else scheme, | |
2233 split_result.netloc.decode("utf-8"), | |
2234 os.path.join(split_result.path, path), | |
2235 query, | |
2236 fragment, | |
2237 ) | |
2238 ) | |
2239 | |
2240 def checkRedirection(self, url): | |
2241 """check is a part of the URL prefix is redirected then replace it | |
2242 | |
2243 @param url(unicode): url to check | |
2244 @return (unicode): possibly redirected URL which should link to the same location | |
2245 """ | |
2246 inv_redirections = self.root.inv_redirections | |
2247 url_parts = url.strip(u"/").split(u"/") | |
2248 for idx in xrange(len(url), 0, -1): | |
2249 test_url = u"/" + u"/".join(url_parts[:idx]) | |
2250 if test_url in inv_redirections: | |
2251 rem_url = url_parts[idx:] | |
2252 return os.path.join( | |
2253 u"/", u"/".join([inv_redirections[test_url]] + rem_url) | |
2254 ) | |
2255 return url | |
2256 | |
2257 ## Sessions ## | |
2258 | |
2259 def purgeSession(self, request): | |
2260 """helper method to purge a session during request handling""" | |
2261 session = request.session | |
2262 if session is not None: | |
2263 log.debug(_(u"session purge")) | |
2264 session.expire() | |
2265 # FIXME: not clean but it seems that it's the best way to reset | |
2266 # session during request handling | |
2267 request._secureSession = request._insecureSession = None | |
2268 | |
2269 def getSessionData(self, request, *args): | |
2270 """helper method to retrieve session data | |
2271 | |
2272 @param request(server.Request): request linked to the session | |
2273 @param *args(zope.interface.Interface): interface of the session to get | |
2274 @return (iterator(data)): requested session data | |
2275 """ | |
2276 session = request.getSession() | |
2277 if len(args) == 1: | |
2278 return args[0](session) | |
2279 else: | |
2280 return (iface(session) for iface in args) | |
2281 | |
2282 @defer.inlineCallbacks | |
2283 def getAffiliation(self, request, service, node): | |
2284 """retrieve pubsub node affiliation for current user | |
2285 | |
2286 use cache first, and request pubsub service if not cache is found | |
2287 @param request(server.Request): request linked to the session | |
2288 @param service(jid.JID): pubsub service | |
2289 @param node(unicode): pubsub node | |
2290 @return (unicode): affiliation | |
2291 """ | |
2292 sat_session = self.getSessionData(request, session_iface.ISATSession) | |
2293 if sat_session.profile is None: | |
2294 raise exceptions.InternalError(u"profile must be set to use this method") | |
2295 affiliation = sat_session.getAffiliation(service, node) | |
2296 if affiliation is not None: | |
2297 defer.returnValue(affiliation) | |
2298 else: | |
2299 try: | |
2300 affiliations = yield self.bridgeCall( | |
2301 "psAffiliationsGet", service.full(), node, sat_session.profile | |
2302 ) | |
2303 except Exception as e: | |
2304 log.warning( | |
2305 "Can't retrieve affiliation for {service}/{node}: {reason}".format( | |
2306 service=service, node=node, reason=e | |
2307 ) | |
2308 ) | |
2309 affiliation = u"" | |
2310 else: | |
2311 try: | |
2312 affiliation = affiliations[node] | |
2313 except KeyError: | |
2314 affiliation = u"" | |
2315 sat_session.setAffiliation(service, node, affiliation) | |
2316 defer.returnValue(affiliation) | |
2317 | |
2318 ## Websocket (dynamic pages) ## | |
2319 | |
2320 def getWebsocketURL(self, request): | |
2321 base_url_split = self.getExtBaseURLData(request) | |
2322 if base_url_split.scheme.endswith("s"): | |
2323 scheme = u"wss" | |
2324 else: | |
2325 scheme = u"ws" | |
2326 | |
2327 return self.getExtBaseURL(request, path=scheme, scheme=scheme) | |
2328 | |
2329 def registerWSToken(self, token, page, request): | |
2330 websockets.LiberviaPageWSProtocol.registerToken(token, page, request) | |
2331 | |
2332 ## Various utils ## | |
2333 | |
2334 def getHTTPDate(self, timestamp=None): | |
2335 now = time.gmtime(timestamp) | |
2336 fmt_date = u"{day_name}, %d {month_name} %Y %H:%M:%S GMT".format( | |
2337 day_name=C.HTTP_DAYS[now.tm_wday], month_name=C.HTTP_MONTH[now.tm_mon - 1] | |
2338 ) | |
2339 return time.strftime(fmt_date, now) | |
2340 | |
2341 ## TLS related methods ## | |
2342 | |
2343 def _TLSOptionsCheck(self): | |
2344 """Check options coherence if TLS is activated, and update missing values | |
2345 | |
2346 Must be called only if TLS is activated | |
2347 """ | |
2348 if not self.options["tls_certificate"]: | |
2349 log.error(u"a TLS certificate is needed to activate HTTPS connection") | |
2350 self.quit(1) | |
2351 if not self.options["tls_private_key"]: | |
2352 self.options["tls_private_key"] = self.options["tls_certificate"] | |
2353 | |
2354 if not self.options["tls_private_key"]: | |
2355 self.options["tls_private_key"] = self.options["tls_certificate"] | |
2356 | |
2357 def _loadCertificates(self, f): | |
2358 """Read a .pem file with a list of certificates | |
2359 | |
2360 @param f (file): file obj (opened .pem file) | |
2361 @return (list[OpenSSL.crypto.X509]): list of certificates | |
2362 @raise OpenSSL.crypto.Error: error while parsing the file | |
2363 """ | |
2364 # XXX: didn't found any method to load a .pem file with several certificates | |
2365 # so the certificates split is done here | |
2366 certificates = [] | |
2367 buf = [] | |
2368 while True: | |
2369 line = f.readline() | |
2370 buf.append(line) | |
2371 if "-----END CERTIFICATE-----" in line: | |
2372 certificates.append( | |
2373 OpenSSL.crypto.load_certificate( | |
2374 OpenSSL.crypto.FILETYPE_PEM, "".join(buf) | |
2375 ) | |
2376 ) | |
2377 buf = [] | |
2378 elif not line: | |
2379 log.debug(u"{} certificate(s) found".format(len(certificates))) | |
2380 return certificates | |
2381 | |
2382 def _loadPKey(self, f): | |
2383 """Read a private key from a .pem file | |
2384 | |
2385 @param f (file): file obj (opened .pem file) | |
2386 @return (list[OpenSSL.crypto.PKey]): private key object | |
2387 @raise OpenSSL.crypto.Error: error while parsing the file | |
2388 """ | |
2389 return OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, f.read()) | |
2390 | |
2391 def _loadCertificate(self, f): | |
2392 """Read a public certificate from a .pem file | |
2393 | |
2394 @param f (file): file obj (opened .pem file) | |
2395 @return (list[OpenSSL.crypto.X509]): public certificate | |
2396 @raise OpenSSL.crypto.Error: error while parsing the file | |
2397 """ | |
2398 return OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, f.read()) | |
2399 | |
2400 def _getTLSContextFactory(self): | |
2401 """Load TLS certificate and build the context factory needed for listenSSL""" | |
2402 if ssl is None: | |
2403 raise ImportError(u"Python module pyOpenSSL is not installed!") | |
2404 | |
2405 cert_options = {} | |
2406 | |
2407 for name, option, method in [ | |
2408 ("privateKey", "tls_private_key", self._loadPKey), | |
2409 ("certificate", "tls_certificate", self._loadCertificate), | |
2410 ("extraCertChain", "tls_chain", self._loadCertificates), | |
2411 ]: | |
2412 path = self.options[option] | |
2413 if not path: | |
2414 assert option == "tls_chain" | |
2415 continue | |
2416 log.debug(u"loading {option} from {path}".format(option=option, path=path)) | |
2417 try: | |
2418 with open(path) as f: | |
2419 cert_options[name] = method(f) | |
2420 except IOError as e: | |
2421 log.error( | |
2422 u"Error while reading file {path} for option {option}: {error}".format( | |
2423 path=path, option=option, error=e | |
2424 ) | |
2425 ) | |
2426 self.quit(2) | |
2427 except OpenSSL.crypto.Error: | |
2428 log.error( | |
2429 u"Error while parsing file {path} for option {option}, are you sure it is a valid .pem file?".format( | |
2430 path=path, option=option | |
2431 ) | |
2432 ) | |
2433 if ( | |
2434 option == "tls_private_key" | |
2435 and self.options["tls_certificate"] == path | |
2436 ): | |
2437 log.error( | |
2438 u"You are using the same file for private key and public certificate, make sure that both a in {path} or use --tls_private_key option".format( | |
2439 path=path | |
2440 ) | |
2441 ) | |
2442 self.quit(2) | |
2443 | |
2444 return ssl.CertificateOptions(**cert_options) | |
2445 | |
2446 ## service management ## | |
2447 | |
2448 def _startService(self, dummy=None): | |
2449 """Actually start the HTTP(S) server(s) after the profile for Libervia is connected. | |
2450 | |
2451 @raise ImportError: OpenSSL is not available | |
2452 @raise IOError: the certificate file doesn't exist | |
2453 @raise OpenSSL.crypto.Error: the certificate file is invalid | |
2454 """ | |
2455 # now that we have service profile connected, we add resource for its cache | |
2456 service_path = regex.pathEscape(C.SERVICE_PROFILE) | |
2457 cache_dir = os.path.join(self.cache_root_dir, u"profiles", service_path) | |
2458 self.cache_resource.putChild(service_path, ProtectedFile(cache_dir)) | |
2459 self.service_cache_url = u"/" + os.path.join(C.CACHE_DIR, service_path) | |
2460 session_iface.SATSession.service_cache_url = self.service_cache_url | |
2461 | |
2462 if self.options["connection_type"] in ("https", "both"): | |
2463 self._TLSOptionsCheck() | |
2464 context_factory = self._getTLSContextFactory() | |
2465 reactor.listenSSL(self.options["port_https"], self.site, context_factory) | |
2466 if self.options["connection_type"] in ("http", "both"): | |
2467 if ( | |
2468 self.options["connection_type"] == "both" | |
2469 and self.options["redirect_to_https"] | |
2470 ): | |
2471 reactor.listenTCP( | |
2472 self.options["port"], | |
2473 server.Site( | |
2474 RedirectToHTTPS( | |
2475 self.options["port"], self.options["port_https_ext"] | |
2476 ) | |
2477 ), | |
2478 ) | |
2479 else: | |
2480 reactor.listenTCP(self.options["port"], self.site) | |
2481 | |
2482 @defer.inlineCallbacks | |
2483 def stopService(self): | |
2484 log.info(_("launching cleaning methods")) | |
2485 for callback, args, kwargs in self._cleanup: | |
2486 callback(*args, **kwargs) | |
2487 try: | |
2488 yield self.bridgeCall("disconnect", C.SERVICE_PROFILE) | |
2489 except Exception: | |
2490 log.warning(u"Can't disconnect service profile") | |
2491 | |
2492 def run(self): | |
2493 reactor.run() | |
2494 | |
2495 def stop(self): | |
2496 reactor.stop() | |
2497 | |
2498 def quit(self, exit_code=None): | |
2499 """Exit app when reactor is running | |
2500 | |
2501 @param exit_code(None, int): exit code | |
2502 """ | |
2503 self.stop() | |
2504 sys.exit(exit_code or 0) | |
2505 | |
2506 | |
2507 class RedirectToHTTPS(web_resource.Resource): | |
2508 def __init__(self, old_port, new_port): | |
2509 web_resource.Resource.__init__(self) | |
2510 self.isLeaf = True | |
2511 self.old_port = old_port | |
2512 self.new_port = new_port | |
2513 | |
2514 def render(self, request): | |
2515 netloc = request.URLPath().netloc.replace( | |
2516 ":%s" % self.old_port, ":%s" % self.new_port | |
2517 ) | |
2518 url = "https://" + netloc + request.uri | |
2519 return web_util.redirectTo(url, request) | |
2520 | |
2521 | |
2522 registerAdapter(session_iface.SATSession, server.Session, session_iface.ISATSession) | |
2523 registerAdapter( | |
2524 session_iface.SATGuestSession, server.Session, session_iface.ISATGuestSession | |
2525 ) |