comparison libervia/web/server/server.py @ 1518:eb00d593801d

refactoring: rename `libervia` to `libervia.web` + update imports following backend changes
author Goffi <goffi@goffi.org>
date Fri, 02 Jun 2023 16:49:28 +0200
parents libervia/server/server.py@a3ca1bab6eb1
children 01b8d68edd70
comparison
equal deleted inserted replaced
1517:b8ed9726525b 1518:eb00d593801d
1 #!/usr/bin/env python3
2
3 # Libervia Web
4 # Copyright (C) 2011-2021 Jérôme Poisson <goffi@goffi.org>
5
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Affero General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Affero General Public License for more details.
15
16 # You should have received a copy of the GNU Affero General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19 from functools import partial
20 import os.path
21 from pathlib import Path
22 import re
23 import sys
24 import time
25 from typing import Callable, Dict, Optional
26 import urllib.error
27 import urllib.parse
28 import urllib.request
29
30 from twisted.application import service
31 from twisted.internet import defer, inotify, reactor
32 from twisted.python import failure
33 from twisted.python import filepath
34 from twisted.python.components import registerAdapter
35 from twisted.web import server
36 from twisted.web import static
37 from twisted.web import resource as web_resource
38 from twisted.web import util as web_util
39 from twisted.web import vhost
40 from twisted.words.protocols.jabber import jid
41
42 import libervia.web
43 from libervia.web.server import websockets
44 from libervia.web.server import session_iface
45 from libervia.web.server.constants import Const as C
46 from libervia.web.server.pages import LiberviaPage
47 from libervia.web.server.tasks.manager import TasksManager
48 from libervia.web.server.utils import ProgressHandler
49 from libervia.backend.core import exceptions
50 from libervia.backend.core.i18n import _
51 from libervia.backend.core.log import getLogger
52 from libervia.backend.tools import utils
53 from libervia.backend.tools import config
54 from libervia.backend.tools.common import regex
55 from libervia.backend.tools.common import template
56 from libervia.backend.tools.common import data_format
57 from libervia.backend.tools.common import tls
58 from libervia.frontends.bridge.bridge_frontend import BridgeException
59 from libervia.frontends.bridge.dbus_bridge import BridgeExceptionNoService, bridge
60 from libervia.frontends.bridge.dbus_bridge import const_TIMEOUT as BRIDGE_TIMEOUT
61
62 from .resources import LiberviaRootResource, ProtectedFile
63 from .restricted_bridge import RestrictedBridge
64
65 log = getLogger(__name__)
66
67
68 DEFAULT_MASK = (inotify.IN_CREATE | inotify.IN_MODIFY | inotify.IN_MOVE_SELF
69 | inotify.IN_MOVED_TO)
70
71
72 class SysExit(Exception):
73
74 def __init__(self, exit_code, message=""):
75 self.exit_code = exit_code
76 self.message = message
77
78 def __str__(self):
79 return f"System Exit({self.exit_code}): {self.message}"
80
81
82 class FilesWatcher(object):
83 """Class to check files modifications using iNotify"""
84 _notifier = None
85
86 def __init__(self, host):
87 self.host = host
88
89 @property
90 def notifier(self):
91 if self._notifier == None:
92 notifier = self.__class__._notifier = inotify.INotify()
93 notifier.startReading()
94 return self._notifier
95
96 def _check_callback(self, dir_path, callback, recursive):
97 # Twisted doesn't add callback if a watcher was already set on a path
98 # but in dev mode Libervia watches whole sites + internal path can be watched
99 # by tasks, so several callbacks must be called on some paths.
100 # This method check that the new callback is indeed present in the desired path
101 # and add it otherwise.
102 # FIXME: this should probably be fixed upstream
103 if recursive:
104 for child in dir_path.walk():
105 if child.isdir():
106 self._check_callback(child, callback, recursive=False)
107 else:
108 watch_id = self.notifier._isWatched(dir_path)
109 if watch_id is None:
110 log.warning(
111 f"There is no watch ID for path {dir_path}, this should not happen"
112 )
113 else:
114 watch_point = self.notifier._watchpoints[watch_id]
115 if callback not in watch_point.callbacks:
116 watch_point.callbacks.append(callback)
117
118 def watch_dir(self, dir_path, callback, mask=DEFAULT_MASK, auto_add=False,
119 recursive=False, **kwargs):
120 dir_path = str(dir_path)
121 log.info(_("Watching directory {dir_path}").format(dir_path=dir_path))
122 wrapped_callback = lambda __, filepath, mask: callback(
123 self.host, filepath, inotify.humanReadableMask(mask), **kwargs)
124 callbacks = [wrapped_callback]
125 dir_path = filepath.FilePath(dir_path)
126 self.notifier.watch(
127 dir_path, mask=mask, autoAdd=auto_add, recursive=recursive,
128 callbacks=callbacks)
129 self._check_callback(dir_path, wrapped_callback, recursive)
130
131
132 class WebSession(server.Session):
133 sessionTimeout = C.SESSION_TIMEOUT
134
135 def __init__(self, *args, **kwargs):
136 self.__lock = False
137 server.Session.__init__(self, *args, **kwargs)
138
139 def lock(self):
140 """Prevent session from expiring"""
141 self.__lock = True
142 self._expireCall.reset(sys.maxsize)
143
144 def unlock(self):
145 """Allow session to expire again, and touch it"""
146 self.__lock = False
147 self.touch()
148
149 def touch(self):
150 if not self.__lock:
151 server.Session.touch(self)
152
153
154 class WaitingRequests(dict):
155 def set_request(self, request, profile, register_with_ext_jid=False):
156 """Add the given profile to the waiting list.
157
158 @param request (server.Request): the connection request
159 @param profile (str): %(doc_profile)s
160 @param register_with_ext_jid (bool): True if we will try to register the
161 profile with an external XMPP account credentials
162 """
163 dc = reactor.callLater(BRIDGE_TIMEOUT, self.purge_request, profile)
164 self[profile] = (request, dc, register_with_ext_jid)
165
166 def purge_request(self, profile):
167 """Remove the given profile from the waiting list.
168
169 @param profile (str): %(doc_profile)s
170 """
171 try:
172 dc = self[profile][1]
173 except KeyError:
174 return
175 if dc.active():
176 dc.cancel()
177 del self[profile]
178
179 def get_request(self, profile):
180 """Get the waiting request for the given profile.
181
182 @param profile (str): %(doc_profile)s
183 @return: the waiting request or None
184 """
185 return self[profile][0] if profile in self else None
186
187 def get_register_with_ext_jid(self, profile):
188 """Get the value of the register_with_ext_jid parameter.
189
190 @param profile (str): %(doc_profile)s
191 @return: bool or None
192 """
193 return self[profile][2] if profile in self else None
194
195
196 class LiberviaWeb(service.Service):
197 debug = defer.Deferred.debug # True if twistd/Libervia is launched in debug mode
198
199 def __init__(self, options):
200 self.options = options
201 websockets.host = self
202
203 def _init(self):
204 # we do init here and not in __init__ to avoid doule initialisation with twistd
205 # this _init is called in startService
206 self.initialised = defer.Deferred()
207 self.waiting_profiles = WaitingRequests() # FIXME: should be removed
208 self._main_conf = None
209 self.files_watcher = FilesWatcher(self)
210
211 if self.options["base_url_ext"]:
212 self.base_url_ext = self.options.pop("base_url_ext")
213 if self.base_url_ext[-1] != "/":
214 self.base_url_ext += "/"
215 self.base_url_ext_data = urllib.parse.urlsplit(self.base_url_ext)
216 else:
217 self.base_url_ext = None
218 # we split empty string anyway so we can do things like
219 # scheme = self.base_url_ext_data.scheme or 'https'
220 self.base_url_ext_data = urllib.parse.urlsplit("")
221
222 if not self.options["port_https_ext"]:
223 self.options["port_https_ext"] = self.options["port_https"]
224
225 self._cleanup = []
226
227 self.sessions = {} # key = session value = user
228 self.prof_connected = set() # Profiles connected
229 self.ns_map = {} # map of short name to namespaces
230
231 ## bridge ##
232 self._bridge_retry = self.options['bridge-retries']
233 self.bridge = bridge()
234 self.bridge.bridge_connect(callback=self._bridge_cb, errback=self._bridge_eb)
235
236 ## libervia app callbacks ##
237 # mapping instance id to the callback to call on "started" signal
238 self.apps_cb: Dict[str, Callable] = {}
239
240 @property
241 def roots(self):
242 """Return available virtual host roots
243
244 Root resources are only returned once, even if they are present for multiple
245 named vhosts. Order is not relevant, except for default vhost which is always
246 returned first.
247 @return (list[web_resource.Resource]): all vhost root resources
248 """
249 roots = list(set(self.vhost_root.hosts.values()))
250 default = self.vhost_root.default
251 if default is not None and default not in roots:
252 roots.insert(0, default)
253 return roots
254
255 @property
256 def main_conf(self):
257 """SafeConfigParser instance opened on configuration file (libervia.conf)"""
258 if self._main_conf is None:
259 self._main_conf = config.parse_main_conf(log_filenames=True)
260 return self._main_conf
261
262 def config_get(self, site_root_res, key, default=None, value_type=None):
263 """Retrieve configuration associated to a site
264
265 Section is automatically set to site name
266 @param site_root_res(LiberviaRootResource): resource of the site in use
267 @param key(unicode): key to use
268 @param default: value to use if not found (see [config.config_get])
269 @param value_type(unicode, None): filter to use on value
270 Note that filters are already automatically used when the key finish
271 by a well known suffix ("_path", "_list", "_dict", or "_json")
272 None to use no filter, else can be:
273 - "path": a path is expected, will be normalized and expanded
274
275 """
276 section = site_root_res.site_name.lower().strip() or C.CONFIG_SECTION
277 value = config.config_get(self.main_conf, section, key, default=default)
278 if value_type is not None:
279 if value_type == 'path':
280 v_filter = lambda v: os.path.abspath(os.path.expanduser(v))
281 else:
282 raise ValueError("unknown value type {value_type}".format(
283 value_type = value_type))
284 if isinstance(value, list):
285 value = [v_filter(v) for v in value]
286 elif isinstance(value, dict):
287 value = {k:v_filter(v) for k,v in list(value.items())}
288 elif value is not None:
289 value = v_filter(value)
290 return value
291
292 def _namespaces_get_cb(self, ns_map):
293 self.ns_map = {str(k): str(v) for k,v in ns_map.items()}
294
295 def _namespaces_get_eb(self, failure_):
296 log.error(_("Can't get namespaces map: {msg}").format(msg=failure_))
297
298 @template.contextfilter
299 def _front_url_filter(self, ctx, relative_url):
300 template_data = ctx['template_data']
301 return os.path.join(
302 '/', C.TPL_RESOURCE, template_data.site or C.SITE_NAME_DEFAULT,
303 C.TEMPLATE_TPL_DIR, template_data.theme, relative_url)
304
305 def _move_first_level_to_dict(self, options, key, keys_to_keep):
306 """Read a config option and put value at first level into u'' dict
307
308 This is useful to put values for Libervia official site directly in dictionary,
309 and to use site_name as keys when external sites are used.
310 options will be modified in place
311 @param options(dict): options to modify
312 @param key(unicode): setting key to modify
313 @param keys_to_keep(list(unicode)): keys allowed in first level
314 """
315 try:
316 conf = options[key]
317 except KeyError:
318 return
319 if not isinstance(conf, dict):
320 options[key] = {'': conf}
321 return
322 default_dict = conf.get('', {})
323 to_delete = []
324 for key, value in conf.items():
325 if key not in keys_to_keep:
326 default_dict[key] = value
327 to_delete.append(key)
328 for key in to_delete:
329 del conf[key]
330 if default_dict:
331 conf[''] = default_dict
332
333 async def check_and_connect_service_profile(self):
334 passphrase = self.options["passphrase"]
335 if not passphrase:
336 raise SysExit(
337 C.EXIT_BAD_ARG,
338 _("No passphrase set for service profile, please check installation "
339 "documentation.")
340 )
341 try:
342 s_prof_connected = await self.bridge_call("is_connected", C.SERVICE_PROFILE)
343 except BridgeException as e:
344 if e.classname == "ProfileUnknownError":
345 log.info("Service profile doesn't exist, creating it.")
346 try:
347 xmpp_domain = await self.bridge_call("config_get", "", "xmpp_domain")
348 xmpp_domain = xmpp_domain.strip()
349 if not xmpp_domain:
350 raise SysExit(
351 C.EXIT_BAD_ARG,
352 _('"xmpp_domain" must be set to create new accounts, please '
353 'check documentation')
354 )
355 service_profile_jid_s = f"{C.SERVICE_PROFILE}@{xmpp_domain}"
356 await self.bridge_call(
357 "in_band_account_new",
358 service_profile_jid_s,
359 passphrase,
360 "",
361 xmpp_domain,
362 0,
363 )
364 except BridgeException as e:
365 if e.condition == "conflict":
366 log.info(
367 _("Service's profile JID {profile_jid} already exists")
368 .format(profile_jid=service_profile_jid_s)
369 )
370 elif e.classname == "UnknownMethod":
371 raise SysExit(
372 C.EXIT_BRIDGE_ERROR,
373 _("Can't create service profile XMPP account, In-Band "
374 "Registration plugin is not activated, you'll have to "
375 "create the {profile!r} profile with {profile_jid!r} JID "
376 "manually.").format(
377 profile=C.SERVICE_PROFILE,
378 profile_jid=service_profile_jid_s)
379 )
380 elif e.condition == "service-unavailable":
381 raise SysExit(
382 C.EXIT_BRIDGE_ERROR,
383 _("Can't create service profile XMPP account, In-Band "
384 "Registration is not activated on your server, you'll have "
385 "to create the {profile!r} profile with {profile_jid!r} JID "
386 "manually.\nNote that you'll need to activate In-Band "
387 "Registation on your server if you want users to be able "
388 "to create new account from {app_name}, please check "
389 "documentation.").format(
390 profile=C.SERVICE_PROFILE,
391 profile_jid=service_profile_jid_s,
392 app_name=C.APP_NAME)
393 )
394 elif e.condition == "not-acceptable":
395 raise SysExit(
396 C.EXIT_BRIDGE_ERROR,
397 _("Can't create service profile XMPP account, your XMPP "
398 "server doesn't allow us to create new accounts with "
399 "In-Band Registration please check XMPP server "
400 "configuration: {reason}"
401 ).format(
402 profile=C.SERVICE_PROFILE,
403 profile_jid=service_profile_jid_s,
404 reason=e.message)
405 )
406
407 else:
408 raise SysExit(
409 C.EXIT_BRIDGE_ERROR,
410 _("Can't create service profile XMPP account, you'll have "
411 "do to it manually: {reason}").format(reason=e.message)
412 )
413 try:
414 await self.bridge_call("profile_create", C.SERVICE_PROFILE, passphrase)
415 await self.bridge_call(
416 "profile_start_session", passphrase, C.SERVICE_PROFILE)
417 await self.bridge_call(
418 "param_set", "JabberID", service_profile_jid_s, "Connection", -1,
419 C.SERVICE_PROFILE)
420 await self.bridge_call(
421 "param_set", "Password", passphrase, "Connection", -1,
422 C.SERVICE_PROFILE)
423 except BridgeException as e:
424 raise SysExit(
425 C.EXIT_BRIDGE_ERROR,
426 _("Can't create service profile XMPP account, you'll have "
427 "do to it manually: {reason}").format(reason=e.message)
428 )
429 log.info(_("Service profile has been successfully created"))
430 s_prof_connected = False
431 else:
432 raise SysExit(C.EXIT_BRIDGE_ERROR, e.message)
433
434 if not s_prof_connected:
435 try:
436 await self.bridge_call(
437 "connect",
438 C.SERVICE_PROFILE,
439 passphrase,
440 {},
441 )
442 except BridgeException as e:
443 raise SysExit(
444 C.EXIT_BRIDGE_ERROR,
445 _("Connection of service profile failed: {reason}").format(reason=e)
446 )
447
448 async def backend_ready(self):
449 log.info(f"Libervia Web v{self.full_version}")
450
451 # settings
452 if self.options['dev-mode']:
453 log.info(_("Developer mode activated"))
454 self.media_dir = await self.bridge_call("config_get", "", "media_dir")
455 self.local_dir = await self.bridge_call("config_get", "", "local_dir")
456 self.cache_root_dir = os.path.join(self.local_dir, C.CACHE_DIR)
457 self.renderer = template.Renderer(self, self._front_url_filter)
458 sites_names = list(self.renderer.sites_paths.keys())
459
460 self._move_first_level_to_dict(self.options, "url_redirections_dict", sites_names)
461 self._move_first_level_to_dict(self.options, "menu_json", sites_names)
462 self._move_first_level_to_dict(self.options, "menu_extra_json", sites_names)
463 menu = self.options["menu_json"]
464 if not '' in menu:
465 menu[''] = C.DEFAULT_MENU
466 for site, value in self.options["menu_extra_json"].items():
467 menu[site].extend(value)
468
469 # service profile
470 if not self.options['build-only']:
471 await self.check_and_connect_service_profile()
472
473 # restricted bridge, the one used by browser code
474 self.restricted_bridge = RestrictedBridge(self)
475
476 # we create virtual hosts and import Libervia pages into them
477 self.vhost_root = vhost.NameVirtualHost()
478 default_site_path = Path(libervia.web.__file__).parent.resolve()
479 # self.sat_root is official Libervia site
480 root_path = default_site_path / C.TEMPLATE_STATIC_DIR
481 self.sat_root = default_root = LiberviaRootResource(
482 host=self, host_name='', site_name='',
483 site_path=default_site_path, path=root_path)
484 if self.options['dev-mode']:
485 self.files_watcher.watch_dir(
486 default_site_path, auto_add=True, recursive=True,
487 callback=LiberviaPage.on_file_change, site_root=self.sat_root,
488 site_path=default_site_path)
489 LiberviaPage.import_pages(self, self.sat_root)
490 tasks_manager = TasksManager(self, self.sat_root)
491 await tasks_manager.parse_tasks()
492 await tasks_manager.run_tasks()
493 # FIXME: handle _set_menu in a more generic way, taking care of external sites
494 await self.sat_root._set_menu(self.options["menu_json"])
495 self.vhost_root.default = default_root
496 existing_vhosts = {b'': default_root}
497
498 for host_name, site_name in self.options["vhosts_dict"].items():
499 if site_name == C.SITE_NAME_DEFAULT:
500 raise ValueError(
501 f"{C.DEFAULT_SITE_NAME} is reserved and can't be used in vhosts_dict")
502 encoded_site_name = site_name.encode('utf-8')
503 try:
504 site_path = self.renderer.sites_paths[site_name]
505 except KeyError:
506 log.warning(_(
507 "host {host_name} link to non existing site {site_name}, ignoring "
508 "it").format(host_name=host_name, site_name=site_name))
509 continue
510 if encoded_site_name in existing_vhosts:
511 # we have an alias host, we re-use existing resource
512 res = existing_vhosts[encoded_site_name]
513 else:
514 # for root path we first check if there is a global static dir
515 # if not, we use default template's static dir
516 root_path = os.path.join(site_path, C.TEMPLATE_STATIC_DIR)
517 if not os.path.isdir(root_path):
518 root_path = os.path.join(
519 site_path, C.TEMPLATE_TPL_DIR, C.TEMPLATE_THEME_DEFAULT,
520 C.TEMPLATE_STATIC_DIR)
521 res = LiberviaRootResource(
522 host=self,
523 host_name=host_name,
524 site_name=site_name,
525 site_path=site_path,
526 path=root_path)
527
528 existing_vhosts[encoded_site_name] = res
529
530 if self.options['dev-mode']:
531 self.files_watcher.watch_dir(
532 site_path, auto_add=True, recursive=True,
533 callback=LiberviaPage.on_file_change, site_root=res,
534 # FIXME: site_path should always be a Path, check code above and
535 # in template module
536 site_path=Path(site_path))
537
538 LiberviaPage.import_pages(self, res)
539 # FIXME: default pages are accessible if not overriden by external website
540 # while necessary for login or re-using existing pages
541 # we may want to disable access to the page by direct URL
542 # (e.g. /blog disabled except if called by external site)
543 LiberviaPage.import_pages(self, res, root_path=default_site_path)
544 tasks_manager = TasksManager(self, res)
545 await tasks_manager.parse_tasks()
546 await tasks_manager.run_tasks()
547 await res._set_menu(self.options["menu_json"])
548
549 self.vhost_root.addHost(host_name.encode('utf-8'), res)
550
551 templates_res = web_resource.Resource()
552 self.put_child_all(C.TPL_RESOURCE.encode('utf-8'), templates_res)
553 for site_name, site_path in self.renderer.sites_paths.items():
554 templates_res.putChild(site_name.encode() or C.SITE_NAME_DEFAULT.encode(),
555 static.File(site_path))
556
557 d = self.bridge_call("namespaces_get")
558 d.addCallback(self._namespaces_get_cb)
559 d.addErrback(self._namespaces_get_eb)
560
561 # websocket
562 if self.options["connection_type"] in ("https", "both"):
563 wss = websockets.LiberviaPageWSProtocol.get_resource(secure=True)
564 self.put_child_all(b'wss', wss)
565 if self.options["connection_type"] in ("http", "both"):
566 ws = websockets.LiberviaPageWSProtocol.get_resource(secure=False)
567 self.put_child_all(b'ws', ws)
568
569 # following signal is needed for cache handling in Libervia pages
570 self.bridge.register_signal(
571 "ps_event_raw", partial(LiberviaPage.on_node_event, self), "plugin"
572 )
573 self.bridge.register_signal(
574 "message_new", partial(self.on_signal, "message_new")
575 )
576 self.bridge.register_signal(
577 "call_accepted", partial(self.on_signal, "call_accepted"), "plugin"
578 )
579 self.bridge.register_signal(
580 "call_ended", partial(self.on_signal, "call_ended"), "plugin"
581 )
582 self.bridge.register_signal(
583 "ice_candidates_new", partial(self.on_signal, "ice_candidates_new"), "plugin"
584 )
585 self.bridge.register_signal(
586 "action_new", self.action_new_handler,
587 )
588
589 # libervia applications handling
590 self.bridge.register_signal(
591 "application_started", self.application_started_handler, "plugin"
592 )
593 self.bridge.register_signal(
594 "application_error", self.application_error_handler, "plugin"
595 )
596
597 #  Progress handling
598 self.bridge.register_signal(
599 "progress_started", partial(ProgressHandler._signal, "started")
600 )
601 self.bridge.register_signal(
602 "progress_finished", partial(ProgressHandler._signal, "finished")
603 )
604 self.bridge.register_signal(
605 "progress_error", partial(ProgressHandler._signal, "error")
606 )
607
608 # media dirs
609 # FIXME: get rid of dirname and "/" in C.XXX_DIR
610 self.put_child_all(os.path.dirname(C.MEDIA_DIR).encode('utf-8'),
611 ProtectedFile(self.media_dir))
612
613 self.cache_resource = web_resource.NoResource()
614 self.put_child_all(C.CACHE_DIR.encode('utf-8'), self.cache_resource)
615 self.cache_resource.putChild(
616 b"common", ProtectedFile(str(self.cache_root_dir / Path("common"))))
617
618 # redirections
619 for root in self.roots:
620 await root._init_redirections(self.options)
621
622 # no need to keep url_redirections_dict, it will not be used anymore
623 del self.options["url_redirections_dict"]
624
625 server.Request.defaultContentType = "text/html; charset=utf-8"
626 wrapped = web_resource.EncodingResourceWrapper(
627 self.vhost_root, [server.GzipEncoderFactory()]
628 )
629 self.site = server.Site(wrapped)
630 self.site.sessionFactory = WebSession
631
632 def _bridge_cb(self):
633 del self._bridge_retry
634 self.bridge.ready_get(
635 lambda: self.initialised.callback(None),
636 lambda failure: self.initialised.errback(Exception(failure)),
637 )
638 self.initialised.addCallback(lambda __: defer.ensureDeferred(self.backend_ready()))
639
640 def _bridge_eb(self, failure_):
641 if isinstance(failure_, BridgeExceptionNoService):
642 if self._bridge_retry:
643 if self._bridge_retry < 0:
644 print(_("Can't connect to bridge, will retry indefinitely. "
645 "Next try in 1s."))
646 else:
647 self._bridge_retry -= 1
648 print(
649 _(
650 "Can't connect to bridge, will retry in 1 s ({retries_left} "
651 "trie(s) left)."
652 ).format(retries_left=self._bridge_retry)
653 )
654 time.sleep(1)
655 self.bridge.bridge_connect(callback=self._bridge_cb, errback=self._bridge_eb)
656 return
657
658 print("Can't connect to SàT backend, are you sure it's launched ?")
659 else:
660 log.error("Can't connect to bridge: {}".format(failure))
661 sys.exit(1)
662
663 @property
664 def version(self):
665 """Return the short version of Libervia"""
666 return C.APP_VERSION
667
668 @property
669 def full_version(self):
670 """Return the full version of Libervia (with extra data when in dev mode)"""
671 version = self.version
672 if version[-1] == "D":
673 # we are in debug version, we add extra data
674 try:
675 return self._version_cache
676 except AttributeError:
677 self._version_cache = "{} ({})".format(
678 version, utils.get_repository_data(libervia.web)
679 )
680 return self._version_cache
681 else:
682 return version
683
684 def bridge_call(self, method_name, *args, **kwargs):
685 """Call an asynchronous bridge method and return a deferred
686
687 @param method_name: name of the method as a unicode
688 @return: a deferred which trigger the result
689
690 """
691 d = defer.Deferred()
692
693 def _callback(*args):
694 if not args:
695 d.callback(None)
696 else:
697 if len(args) != 1:
698 Exception("Multiple return arguments not supported")
699 d.callback(args[0])
700
701 def _errback(failure_):
702 d.errback(failure.Failure(failure_))
703
704 kwargs["callback"] = _callback
705 kwargs["errback"] = _errback
706 getattr(self.bridge, method_name)(*args, **kwargs)
707 return d
708
709 def action_new_handler(
710 self,
711 action_data_s: str,
712 action_id: str,
713 security_limit: int,
714 profile: str
715 ) -> None:
716 if security_limit > C.SECURITY_LIMIT:
717 log.debug(
718 f"ignoring action {action_id} due to security limit"
719 )
720 else:
721 self.on_signal(
722 "action_new", action_data_s, action_id, security_limit, profile
723 )
724
725 def on_signal(self, signal_name, *args):
726 profile = args[-1]
727 if not profile:
728 log.error(f"got signal without profile: {signal_name}, {args}")
729 return
730 session_iface.WebSession.send(
731 profile,
732 "bridge",
733 {"signal": signal_name, "args": args}
734 )
735
736 def application_started_handler(
737 self,
738 name: str,
739 instance_id: str,
740 extra_s: str
741 ) -> None:
742 callback = self.apps_cb.pop(instance_id, None)
743 if callback is not None:
744 defer.ensureDeferred(callback(str(name), str(instance_id)))
745
746 def application_error_handler(
747 self,
748 name: str,
749 instance_id: str,
750 extra_s: str
751 ) -> None:
752 callback = self.apps_cb.pop(instance_id, None)
753 if callback is not None:
754 extra = data_format.deserialise(extra_s)
755 log.error(
756 f"Can't start application {name}: {extra['class']}\n{extra['msg']}"
757 )
758
759 async def _logged(self, profile, request):
760 """Set everything when a user just logged in
761
762 @param profile
763 @param request
764 @return: a constant indicating the state:
765 - C.PROFILE_LOGGED
766 - C.PROFILE_LOGGED_EXT_JID
767 @raise exceptions.ConflictError: session is already active
768 """
769 register_with_ext_jid = self.waiting_profiles.get_register_with_ext_jid(profile)
770 self.waiting_profiles.purge_request(profile)
771 session = request.getSession()
772 web_session = session_iface.IWebSession(session)
773 if web_session.profile:
774 log.error(_("/!\\ Session has already a profile, this should NEVER happen!"))
775 raise failure.Failure(exceptions.ConflictError("Already active"))
776
777 # XXX: we force string because python D-Bus has its own string type (dbus.String)
778 # which may cause trouble when exposing it to scripts
779 web_session.profile = str(profile)
780 self.prof_connected.add(profile)
781 cache_dir = os.path.join(
782 self.cache_root_dir, "profiles", regex.path_escape(profile)
783 )
784 # FIXME: would be better to have a global /cache URL which redirect to
785 # profile's cache directory, without uuid
786 self.cache_resource.putChild(web_session.uuid.encode('utf-8'),
787 ProtectedFile(cache_dir))
788 log.debug(
789 _("profile cache resource added from {uuid} to {path}").format(
790 uuid=web_session.uuid, path=cache_dir
791 )
792 )
793
794 def on_expire():
795 log.info("Session expired (profile={profile})".format(profile=profile))
796 self.cache_resource.delEntity(web_session.uuid.encode('utf-8'))
797 log.debug(
798 _("profile cache resource {uuid} deleted").format(uuid=web_session.uuid)
799 )
800 web_session.on_expire()
801 if web_session.ws_socket is not None:
802 web_session.ws_socket.close()
803 # and now we disconnect the profile
804 self.bridge_call("disconnect", profile)
805
806 session.notifyOnExpire(on_expire)
807
808 # FIXME: those session infos should be returned by connect or is_connected
809 infos = await self.bridge_call("session_infos_get", profile)
810 web_session.jid = jid.JID(infos["jid"])
811 own_bare_jid_s = web_session.jid.userhost()
812 own_id_raw = await self.bridge_call(
813 "identity_get", own_bare_jid_s, [], True, profile)
814 web_session.identities[own_bare_jid_s] = data_format.deserialise(own_id_raw)
815 web_session.backend_started = int(infos["started"])
816
817 state = C.PROFILE_LOGGED_EXT_JID if register_with_ext_jid else C.PROFILE_LOGGED
818 return state
819
820 @defer.inlineCallbacks
821 def connect(self, request, login, password):
822 """log user in
823
824 If an other user was already logged, it will be unlogged first
825 @param request(server.Request): request linked to the session
826 @param login(unicode): user login
827 can be profile name
828 can be profile@[libervia_domain.ext]
829 can be a jid (a new profile will be created with this jid if needed)
830 @param password(unicode): user password
831 @return (unicode, None): C.SESSION_ACTIVE: if session was aleady active else
832 self._logged value
833 @raise exceptions.DataError: invalid login
834 @raise exceptions.ProfileUnknownError: this login doesn't exist
835 @raise exceptions.PermissionError: a login is not accepted (e.g. empty password
836 not allowed)
837 @raise exceptions.NotReady: a profile connection is already waiting
838 @raise exceptions.TimeoutError: didn't received and answer from bridge
839 @raise exceptions.InternalError: unknown error
840 @raise ValueError(C.PROFILE_AUTH_ERROR): invalid login and/or password
841 @raise ValueError(C.XMPP_AUTH_ERROR): invalid XMPP account password
842 """
843
844 # XXX: all security checks must be done here, even if present in javascript
845 if login.startswith("@"):
846 raise failure.Failure(exceptions.DataError("No profile_key allowed"))
847
848 if login.startswith("guest@@") and login.count("@") == 2:
849 log.debug("logging a guest account")
850 elif "@" in login:
851 if login.count("@") != 1:
852 raise failure.Failure(
853 exceptions.DataError("Invalid login: {login}".format(login=login))
854 )
855 try:
856 login_jid = jid.JID(login)
857 except (RuntimeError, jid.InvalidFormat, AttributeError):
858 raise failure.Failure(exceptions.DataError("No profile_key allowed"))
859
860 # FIXME: should it be cached?
861 new_account_domain = yield self.bridge_call("account_domain_new_get")
862
863 if login_jid.host == new_account_domain:
864 # redirect "user@libervia.org" to the "user" profile
865 login = login_jid.user
866 login_jid = None
867 else:
868 login_jid = None
869
870 try:
871 profile = yield self.bridge_call("profile_name_get", login)
872 except Exception: # XXX: ProfileUnknownError wouldn't work, it's encapsulated
873 # FIXME: find a better way to handle bridge errors
874 if (
875 login_jid is not None and login_jid.user
876 ): # try to create a new libervia.backend profile using the XMPP credentials
877 if not self.options["allow_registration"]:
878 log.warning(
879 "Trying to register JID account while registration is not "
880 "allowed")
881 raise failure.Failure(
882 exceptions.DataError(
883 "JID login while registration is not allowed"
884 )
885 )
886 profile = login # FIXME: what if there is a resource?
887 connect_method = "credentials_xmpp_connect"
888 register_with_ext_jid = True
889 else: # non existing username
890 raise failure.Failure(exceptions.ProfileUnknownError())
891 else:
892 if profile != login or (
893 not password
894 and profile
895 not in self.options["empty_password_allowed_warning_dangerous_list"]
896 ):
897 # profiles with empty passwords are restricted to local frontends
898 raise exceptions.PermissionError
899 register_with_ext_jid = False
900
901 connect_method = "connect"
902
903 # we check if there is not already an active session
904 web_session = session_iface.IWebSession(request.getSession())
905 if web_session.profile:
906 # yes, there is
907 if web_session.profile != profile:
908 # it's a different profile, we need to disconnect it
909 log.warning(_(
910 "{new_profile} requested login, but {old_profile} was already "
911 "connected, disconnecting {old_profile}").format(
912 old_profile=web_session.profile, new_profile=profile))
913 self.purge_session(request)
914
915 if self.waiting_profiles.get_request(profile):
916 #  FIXME: check if and when this can happen
917 raise failure.Failure(exceptions.NotReady("Already waiting"))
918
919 self.waiting_profiles.set_request(request, profile, register_with_ext_jid)
920 try:
921 connected = yield self.bridge_call(connect_method, profile, password)
922 except Exception as failure_:
923 fault = getattr(failure_, 'classname', None)
924 self.waiting_profiles.purge_request(profile)
925 if fault in ("PasswordError", "ProfileUnknownError"):
926 log.info("Profile {profile} doesn't exist or the submitted password is "
927 "wrong".format( profile=profile))
928 raise failure.Failure(ValueError(C.PROFILE_AUTH_ERROR))
929 elif fault == "SASLAuthError":
930 log.info("The XMPP password of profile {profile} is wrong"
931 .format(profile=profile))
932 raise failure.Failure(ValueError(C.XMPP_AUTH_ERROR))
933 elif fault == "NoReply":
934 log.info(_("Did not receive a reply (the timeout expired or the "
935 "connection is broken)"))
936 raise exceptions.TimeOutError
937 elif fault is None:
938 log.info(_("Unexepected failure: {failure_}").format(failure_=failure))
939 raise failure_
940 else:
941 log.error('Unmanaged fault class "{fault}" in errback for the '
942 'connection of profile {profile}'.format(
943 fault=fault, profile=profile))
944 raise failure.Failure(exceptions.InternalError(fault))
945
946 if connected:
947 #  profile is already connected in backend
948 # do we have a corresponding session in Libervia?
949 web_session = session_iface.IWebSession(request.getSession())
950 if web_session.profile:
951 # yes, session is active
952 if web_session.profile != profile:
953 # existing session should have been ended above
954 # so this line should never be reached
955 log.error(_(
956 "session profile [{session_profile}] differs from login "
957 "profile [{profile}], this should not happen!")
958 .format(session_profile=web_session.profile, profile=profile))
959 raise exceptions.InternalError("profile mismatch")
960 defer.returnValue(C.SESSION_ACTIVE)
961 log.info(
962 _(
963 "profile {profile} was already connected in backend".format(
964 profile=profile
965 )
966 )
967 )
968 #  no, we have to create it
969
970 state = yield defer.ensureDeferred(self._logged(profile, request))
971 defer.returnValue(state)
972
973 def register_new_account(self, request, login, password, email):
974 """Create a new account, or return error
975 @param request(server.Request): request linked to the session
976 @param login(unicode): new account requested login
977 @param email(unicode): new account email
978 @param password(unicode): new account password
979 @return(unicode): a constant indicating the state:
980 - C.BAD_REQUEST: something is wrong in the request (bad arguments)
981 - C.INVALID_INPUT: one of the data is not valid
982 - C.REGISTRATION_SUCCEED: new account has been successfully registered
983 - C.ALREADY_EXISTS: the given profile already exists
984 - C.INTERNAL_ERROR or any unmanaged fault string
985 @raise PermissionError: registration is now allowed in server configuration
986 """
987 if not self.options["allow_registration"]:
988 log.warning(
989 _("Registration received while it is not allowed, hack attempt?")
990 )
991 raise failure.Failure(
992 exceptions.PermissionError("Registration is not allowed on this server")
993 )
994
995 if (
996 not re.match(C.REG_LOGIN_RE, login)
997 or not re.match(C.REG_EMAIL_RE, email, re.IGNORECASE)
998 or len(password) < C.PASSWORD_MIN_LENGTH
999 ):
1000 return C.INVALID_INPUT
1001
1002 def registered(result):
1003 return C.REGISTRATION_SUCCEED
1004
1005 def registering_error(failure_):
1006 # FIXME: better error handling for bridge error is needed
1007 status = failure_.value.fullname.split('.')[-1]
1008 if status == "ConflictError":
1009 return C.ALREADY_EXISTS
1010 elif status == "InvalidCertificate":
1011 return C.INVALID_CERTIFICATE
1012 elif status == "InternalError":
1013 return C.INTERNAL_ERROR
1014 else:
1015 log.error(
1016 _("Unknown registering error status: {status}\n{traceback}").format(
1017 status=status, traceback=failure_.value.message
1018 )
1019 )
1020 return status
1021
1022 d = self.bridge_call("libervia_account_register", email, password, login)
1023 d.addCallback(registered)
1024 d.addErrback(registering_error)
1025 return d
1026
1027 def addCleanup(self, callback, *args, **kwargs):
1028 """Add cleaning method to call when service is stopped
1029
1030 cleaning method will be called in reverse order of they insertion
1031 @param callback: callable to call on service stop
1032 @param *args: list of arguments of the callback
1033 @param **kwargs: list of keyword arguments of the callback"""
1034 self._cleanup.insert(0, (callback, args, kwargs))
1035
1036 def init_eb(self, failure):
1037 from twisted.application import app
1038 if failure.check(SysExit):
1039 if failure.value.message:
1040 log.error(failure.value.message)
1041 app._exitCode = failure.value.exit_code
1042 reactor.stop()
1043 else:
1044 log.error(_("Init error: {msg}").format(msg=failure))
1045 app._exitCode = C.EXIT_INTERNAL_ERROR
1046 reactor.stop()
1047 return failure
1048
1049 def _build_only_cb(self, __):
1050 log.info(_("Stopping here due to --build-only flag"))
1051 self.stop()
1052
1053 def startService(self):
1054 """Connect the profile for Libervia and start the HTTP(S) server(s)"""
1055 self._init()
1056 if self.options['build-only']:
1057 self.initialised.addCallback(self._build_only_cb)
1058 else:
1059 self.initialised.addCallback(self._start_service)
1060 self.initialised.addErrback(self.init_eb)
1061
1062 ## URLs ##
1063
1064 def put_child_sat(self, path, resource):
1065 """Add a child to the libervia.backend resource"""
1066 if not isinstance(path, bytes):
1067 raise ValueError("path must be specified in bytes")
1068 self.sat_root.putChild(path, resource)
1069
1070 def put_child_all(self, path, resource):
1071 """Add a child to all vhost root resources"""
1072 if not isinstance(path, bytes):
1073 raise ValueError("path must be specified in bytes")
1074 # we wrap before calling putChild, to avoid having useless multiple instances
1075 # of the resource
1076 # FIXME: check that no information is leaked (c.f. https://twistedmatrix.com/documents/current/web/howto/using-twistedweb.html#request-encoders)
1077 wrapped_res = web_resource.EncodingResourceWrapper(
1078 resource, [server.GzipEncoderFactory()])
1079 for root in self.roots:
1080 root.putChild(path, wrapped_res)
1081
1082 def get_build_path(self, site_name: str, dev: bool=False) -> Path:
1083 """Generate build path for a given site name
1084
1085 @param site_name: name of the site
1086 @param dev: return dev build dir if True, production one otherwise
1087 dev build dir is used for installing dependencies needed temporarily (e.g.
1088 to compile files), while production build path is the one served by the
1089 HTTP server, where final files are downloaded.
1090 @return: path to the build directory
1091 """
1092 sub_dir = C.DEV_BUILD_DIR if dev else C.PRODUCTION_BUILD_DIR
1093 build_path_elts = [
1094 config.config_get(self.main_conf, "", "local_dir"),
1095 C.CACHE_DIR,
1096 C.LIBERVIA_CACHE,
1097 sub_dir,
1098 regex.path_escape(site_name or C.SITE_NAME_DEFAULT)]
1099 build_path = Path("/".join(build_path_elts))
1100 return build_path.expanduser().resolve()
1101
1102 def get_ext_base_url_data(self, request):
1103 """Retrieve external base URL Data
1104
1105 this method try to retrieve the base URL found by external user
1106 It does by checking in this order:
1107 - base_url_ext option from configuration
1108 - proxy x-forwarder-host headers
1109 - URL of the request
1110 @return (urlparse.SplitResult): SplitResult instance with only scheme and
1111 netloc filled
1112 """
1113 ext_data = self.base_url_ext_data
1114 url_path = request.URLPath()
1115
1116 try:
1117 forwarded = request.requestHeaders.getRawHeaders(
1118 "forwarded"
1119 )[0]
1120 except TypeError:
1121 # we try deprecated headers
1122 try:
1123 proxy_netloc = request.requestHeaders.getRawHeaders(
1124 "x-forwarded-host"
1125 )[0]
1126 except TypeError:
1127 proxy_netloc = None
1128 try:
1129 proxy_scheme = request.requestHeaders.getRawHeaders(
1130 "x-forwarded-proto"
1131 )[0]
1132 except TypeError:
1133 proxy_scheme = None
1134 else:
1135 fwd_data = {
1136 k.strip(): v.strip()
1137 for k,v in (d.split("=") for d in forwarded.split(";"))
1138 }
1139 proxy_netloc = fwd_data.get("host")
1140 proxy_scheme = fwd_data.get("proto")
1141
1142 return urllib.parse.SplitResult(
1143 ext_data.scheme or proxy_scheme or url_path.scheme.decode(),
1144 ext_data.netloc or proxy_netloc or url_path.netloc.decode(),
1145 ext_data.path or "/",
1146 "",
1147 "",
1148 )
1149
1150 def get_ext_base_url(
1151 self,
1152 request: server.Request,
1153 path: str = "",
1154 query: str = "",
1155 fragment: str = "",
1156 scheme: Optional[str] = None,
1157 ) -> str:
1158 """Get external URL according to given elements
1159
1160 external URL is the URL seen by external user
1161 @param path: same as for urlsplit.urlsplit
1162 path will be prefixed to follow found external URL if suitable
1163 @param params: same as for urlsplit.urlsplit
1164 @param query: same as for urlsplit.urlsplit
1165 @param fragment: same as for urlsplit.urlsplit
1166 @param scheme: if not None, will override scheme from base URL
1167 @return: external URL
1168 """
1169 split_result = self.get_ext_base_url_data(request)
1170 return urllib.parse.urlunsplit(
1171 (
1172 split_result.scheme if scheme is None else scheme,
1173 split_result.netloc,
1174 os.path.join(split_result.path, path),
1175 query,
1176 fragment,
1177 )
1178 )
1179
1180 def check_redirection(self, vhost_root: LiberviaRootResource, url_path: str) -> str:
1181 """check is a part of the URL prefix is redirected then replace it
1182
1183 @param vhost_root: root of this virtual host
1184 @param url_path: path of the url to check
1185 @return: possibly redirected URL which should link to the same location
1186 """
1187 inv_redirections = vhost_root.inv_redirections
1188 url_parts = url_path.strip("/").split("/")
1189 for idx in range(len(url_parts), -1, -1):
1190 test_url = "/" + "/".join(url_parts[:idx])
1191 if test_url in inv_redirections:
1192 rem_url = url_parts[idx:]
1193 return os.path.join(
1194 "/", "/".join([inv_redirections[test_url]] + rem_url)
1195 )
1196 return url_path
1197
1198 ## Sessions ##
1199
1200 def purge_session(self, request):
1201 """helper method to purge a session during request handling"""
1202 session = request.session
1203 if session is not None:
1204 log.debug(_("session purge"))
1205 web_session = self.get_session_data(request, session_iface.IWebSession)
1206 socket = web_session.ws_socket
1207 if socket is not None:
1208 socket.close()
1209 session.ws_socket = None
1210 session.expire()
1211 # FIXME: not clean but it seems that it's the best way to reset
1212 # session during request handling
1213 request._secureSession = request._insecureSession = None
1214
1215 def get_session_data(self, request, *args):
1216 """helper method to retrieve session data
1217
1218 @param request(server.Request): request linked to the session
1219 @param *args(zope.interface.Interface): interface of the session to get
1220 @return (iterator(data)): requested session data
1221 """
1222 session = request.getSession()
1223 if len(args) == 1:
1224 return args[0](session)
1225 else:
1226 return (iface(session) for iface in args)
1227
1228 @defer.inlineCallbacks
1229 def get_affiliation(self, request, service, node):
1230 """retrieve pubsub node affiliation for current user
1231
1232 use cache first, and request pubsub service if not cache is found
1233 @param request(server.Request): request linked to the session
1234 @param service(jid.JID): pubsub service
1235 @param node(unicode): pubsub node
1236 @return (unicode): affiliation
1237 """
1238 web_session = self.get_session_data(request, session_iface.IWebSession)
1239 if web_session.profile is None:
1240 raise exceptions.InternalError("profile must be set to use this method")
1241 affiliation = web_session.get_affiliation(service, node)
1242 if affiliation is not None:
1243 defer.returnValue(affiliation)
1244 else:
1245 try:
1246 affiliations = yield self.bridge_call(
1247 "ps_affiliations_get", service.full(), node, web_session.profile
1248 )
1249 except Exception as e:
1250 log.warning(
1251 "Can't retrieve affiliation for {service}/{node}: {reason}".format(
1252 service=service, node=node, reason=e
1253 )
1254 )
1255 affiliation = ""
1256 else:
1257 try:
1258 affiliation = affiliations[node]
1259 except KeyError:
1260 affiliation = ""
1261 web_session.set_affiliation(service, node, affiliation)
1262 defer.returnValue(affiliation)
1263
1264 ## Websocket (dynamic pages) ##
1265
1266 def get_websocket_url(self, request):
1267 base_url_split = self.get_ext_base_url_data(request)
1268 if base_url_split.scheme.endswith("s"):
1269 scheme = "wss"
1270 else:
1271 scheme = "ws"
1272
1273 return self.get_ext_base_url(request, path=scheme, scheme=scheme)
1274
1275
1276 ## Various utils ##
1277
1278 def get_http_date(self, timestamp=None):
1279 now = time.gmtime(timestamp)
1280 fmt_date = "{day_name}, %d {month_name} %Y %H:%M:%S GMT".format(
1281 day_name=C.HTTP_DAYS[now.tm_wday], month_name=C.HTTP_MONTH[now.tm_mon - 1]
1282 )
1283 return time.strftime(fmt_date, now)
1284
1285 ## service management ##
1286
1287 def _start_service(self, __=None):
1288 """Actually start the HTTP(S) server(s) after the profile for Libervia is connected.
1289
1290 @raise ImportError: OpenSSL is not available
1291 @raise IOError: the certificate file doesn't exist
1292 @raise OpenSSL.crypto.Error: the certificate file is invalid
1293 """
1294 # now that we have service profile connected, we add resource for its cache
1295 service_path = regex.path_escape(C.SERVICE_PROFILE)
1296 cache_dir = os.path.join(self.cache_root_dir, "profiles", service_path)
1297 self.cache_resource.putChild(service_path.encode('utf-8'),
1298 ProtectedFile(cache_dir))
1299 self.service_cache_url = "/" + os.path.join(C.CACHE_DIR, service_path)
1300 session_iface.WebSession.service_cache_url = self.service_cache_url
1301
1302 if self.options["connection_type"] in ("https", "both"):
1303 try:
1304 tls.tls_options_check(self.options)
1305 context_factory = tls.get_tls_context_factory(self.options)
1306 except exceptions.ConfigError as e:
1307 log.warning(
1308 f"There is a problem in TLS settings in your configuration file: {e}")
1309 self.quit(2)
1310 except exceptions.DataError as e:
1311 log.warning(
1312 f"Can't set TLS: {e}")
1313 self.quit(1)
1314 reactor.listenSSL(self.options["port_https"], self.site, context_factory)
1315 if self.options["connection_type"] in ("http", "both"):
1316 if (
1317 self.options["connection_type"] == "both"
1318 and self.options["redirect_to_https"]
1319 ):
1320 reactor.listenTCP(
1321 self.options["port"],
1322 server.Site(
1323 RedirectToHTTPS(
1324 self.options["port"], self.options["port_https_ext"]
1325 )
1326 ),
1327 )
1328 else:
1329 reactor.listenTCP(self.options["port"], self.site)
1330
1331 @defer.inlineCallbacks
1332 def stopService(self):
1333 log.info(_("launching cleaning methods"))
1334 for callback, args, kwargs in self._cleanup:
1335 callback(*args, **kwargs)
1336 try:
1337 yield self.bridge_call("disconnect", C.SERVICE_PROFILE)
1338 except Exception:
1339 log.warning("Can't disconnect service profile")
1340
1341 def run(self):
1342 reactor.run()
1343
1344 def stop(self):
1345 reactor.stop()
1346
1347 def quit(self, exit_code=None):
1348 """Exit app when reactor is running
1349
1350 @param exit_code(None, int): exit code
1351 """
1352 self.stop()
1353 sys.exit(exit_code or 0)
1354
1355
1356 class RedirectToHTTPS(web_resource.Resource):
1357 def __init__(self, old_port, new_port):
1358 web_resource.Resource.__init__(self)
1359 self.isLeaf = True
1360 self.old_port = old_port
1361 self.new_port = new_port
1362
1363 def render(self, request):
1364 netloc = request.URLPath().netloc.decode().replace(
1365 f":{self.old_port}", f":{self.new_port}"
1366 )
1367 url = f"https://{netloc}{request.uri.decode()}"
1368 return web_util.redirectTo(url.encode(), request)
1369
1370
1371 registerAdapter(session_iface.WebSession, server.Session, session_iface.IWebSession)
1372 registerAdapter(
1373 session_iface.WebGuestSession, server.Session, session_iface.IWebGuestSession
1374 )