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