changeset 1128:6414fd795df4

server, pages: multi-sites refactoring: Libervia is now handling external sites (i.e. other sites than Libervia official site). The external site are declared in sites_path_public_dict (in [DEFAULT] section) which is read by template engine, then they are linked to virtual host with vhosts_dict (linking host name to site name) in [libervia] section. Sites are only instanced once, so adding an alias is just a matter of mapping the alias host name in vhosts_dict with the same site name. menu_json and url_redirections_dict can now accept keys named after site name, which will be linked to the data for the site. Data for default site can still be keyed at first level. Libervia official pages are added to external site (if pages are not overriden), allowing to call pages of the framework and to have facilities like login handling. Deprecated url_redirections_profile option has been removed.
author Goffi <goffi@goffi.org>
date Fri, 14 Sep 2018 21:41:28 +0200
parents 9234f29053b0
children e6fe914c3eaf
files libervia/server/pages.py libervia/server/server.py twisted/plugins/libervia_server.py
diffstat 3 files changed, 595 insertions(+), 448 deletions(-) [+]
line wrap: on
line diff
--- a/libervia/server/pages.py	Sun Sep 09 21:12:22 2018 +0200
+++ b/libervia/server/pages.py	Fri Sep 14 21:41:28 2018 +0200
@@ -25,7 +25,6 @@
 
 from sat.core.i18n import _
 from sat.core import exceptions
-from sat.tools.common import uri as common_uri
 from sat.tools.common import date_utils
 from sat.core.log import getLogger
 
@@ -33,7 +32,6 @@
 from libervia.server.constants import Const as C
 from libervia.server import session_iface
 from libervia.server.utils import quote, SubPage
-import libervia
 
 from collections import namedtuple
 import uuid
@@ -102,65 +100,51 @@
 
 class LiberviaPage(web_resource.Resource):
     isLeaf = True  #  we handle subpages ourself
-    named_pages = {}
-    uri_callbacks = {}
     signals_handlers = {}
-    pages_redirects = {}
     cache = {}
-    cached_urls = {}
     #  Set of tuples (service/node/sub_id) of nodes subscribed for caching
     # sub_id can be empty string if not handled by service
     cache_pubsub_sub = set()
-    main_menu = None
 
     def __init__(
-        self,
-        host,
-        root_dir,
-        url,
-        name=None,
-        redirect=None,
-        access=None,
-        dynamic=False,
-        parse_url=None,
-        prepare_render=None,
-        render=None,
-        template=None,
-        on_data_post=None,
-        on_data=None,
-        on_signal=None,
-        url_cache=False,
-    ):
-        """initiate LiberviaPages
+        self, host, vhost_root, root_dir, url, name=None, redirect=None, access=None,
+        dynamic=False, parse_url=None, prepare_render=None, render=None, template=None,
+        on_data_post=None, on_data=None, on_signal=None, url_cache=False,
+        ):
+        """Initiate LiberviaPage instance
 
         LiberviaPages are the main resources of Libervia, using easy to set python files
-        The arguments are the variables found in page_meta.py
+        The non mandatory arguments are the variables found in page_meta.py
         @param host(Libervia): the running instance of Libervia
+        @param vhost_root(web_resource.Resource): root resource of the virtual host which
+            handle this page.
         @param root_dir(unicode): aboslute file path of the page
         @param url(unicode): relative URL to the page
             this URL may not be valid, as pages may require path arguments
         @param name(unicode, None): if not None, a unique name to identify the page
             can then be used for e.g. redirection
             "/" is not allowed in names (as it can be used to construct URL paths)
-        @param redirect(unicode, None): if not None, this page will be redirected. A redirected
-            parameter is used as in self.pageRedirect. parse_url will not be skipped
+        @param redirect(unicode, None): if not None, this page will be redirected.
+            A redirected parameter is used as in self.pageRedirect.
+            parse_url will not be skipped
             using this redirect parameter is called "full redirection"
-            using self.pageRedirect is called "partial redirection" (because some rendering method
-            can still be used, e.g. parse_url)
+            using self.pageRedirect is called "partial redirection" (because some
+            rendering method can still be used, e.g. parse_url)
         @param access(unicode, None): permission needed to access the page
             None means public access.
-            Pages inherit from parent pages: e.g. if a "settings" page is restricted to admins,
-            and if "settings/blog" is public, it still can only be accessed by admins.
-            see C.PAGES_ACCESS_* for details
+            Pages inherit from parent pages: e.g. if a "settings" page is restricted
+            to admins, and if "settings/blog" is public, it still can only be accessed by
+            admins. See C.PAGES_ACCESS_* for details
         @param dynamic(bool): if True, activate websocket for bidirectional communication
         @param parse_url(callable, None): if set it will be called to handle the URL path
-            after this method, the page will be rendered if noting is left in path (request.postpath)
-            else a the request will be transmitted to a subpage
-        @param prepare_render(callable, None): if set, will be used to prepare the rendering
-            that often means gathering data using the bridge
-        @param render(callable, None): if not template is set, this method will be called and
-            what it returns will be rendered.
-            This method is mutually exclusive with template and must return a unicode string.
+            after this method, the page will be rendered if noting is left in path
+            (request.postpath) else a the request will be transmitted to a subpage
+        @param prepare_render(callable, None): if set, will be used to prepare the
+            rendering. That often means gathering data using the bridge
+        @param render(callable, None): if not template is set, this method will be
+            called and what it returns will be rendered.
+            This method is mutually exclusive with template and must return a unicode
+            string.
         @param template(unicode, None): path to the template to render.
             This method is mutually exclusive with render
         @param on_data_post(callable, None): method to call when data is posted
@@ -169,20 +153,22 @@
                 - C.POST_NO_CONFIRM: confirm flag will not be set
         @param on_data(callable, None): method to call when dynamic data is sent
             this method is used with Libervia's websocket mechanism
-        @param on_signal(callable, None): method to call when a registered signal is received
-            this method is used with Libervia's websocket mechanism
+        @param on_signal(callable, None): method to call when a registered signal is
+            received. This method is used with Libervia's websocket mechanism
+        @param url_cache(boolean): if set, result of parse_url is cached (per profile).
+            Useful when costly calls (e.g. network) are done while parsing URL.
         """
 
         web_resource.Resource.__init__(self)
         self.host = host
+        self.vhost_root = vhost_root
         self.root_dir = root_dir
         self.url = url
         self.name = name
         if name is not None:
             if name in self.named_pages:
                 raise exceptions.ConflictError(
-                    _(u'a Libervia page named "{}" already exists'.format(name))
-                )
+                    _(u'a Libervia page named "{}" already exists'.format(name)))
             if u"/" in name:
                 raise ValueError(_(u'"/" is not allowed in page names'))
             if not name:
@@ -208,11 +194,8 @@
                 for x in (parse_url, prepare_render, render, template)
             ):
                 raise ValueError(
-                    _(
-                        u"you can't use full page redirection with other rendering method,"
-                        u"check self.pageRedirect if you need to use them"
-                    )
-                )
+                    _(u"you can't use full page redirection with other rendering"
+                      u"method, check self.pageRedirect if you need to use them"))
             self.redirect = redirect
         else:
             self.redirect = None
@@ -238,55 +221,110 @@
         self._do_cache = None
 
     def __unicode__(self):
-        return u"LiberviaPage {name} at {url}".format(
-            name=self.name or u"<anonymous>", url=self.url
-        )
+        return u"LiberviaPage {name} at {url} (vhost: {vhost_root})".format(
+            name=self.name or u"<anonymous>", url=self.url, vhost_root=self.vhost_root)
 
     def __str__(self):
         return self.__unicode__().encode("utf-8")
 
+
+    @property
+    def named_pages(self):
+        return self.vhost_root.named_pages
+
+    @property
+    def uri_callbacks(self):
+        return self.vhost_root.uri_callbacks
+
+    @property
+    def pages_redirects(self):
+        return self.vhost_root.pages_redirects
+
+    @property
+    def cached_urls(self):
+        return self.vhost_root.cached_urls
+
+    @property
+    def main_menu(self):
+        return self.vhost_root.main_menu
+
     @classmethod
-    def importPages(cls, host, parent=None, path=None):
-        """Recursively import Libervia pages"""
-        if path is None:
-            path = []
-        if parent is None:
-            root_dir = os.path.join(os.path.dirname(libervia.__file__), C.PAGES_DIR)
-            parent = host
+    def importPages(cls, host, vhost_root, root_path=None, _parent=None, _path=None,
+        _extra_pages=False):
+        """Recursively import Libervia pages
+
+        @param host(Libervia): Libervia instance
+        @param vhost_root(LiberviaRootResource): root of this VirtualHost
+        @param root_path(unicode, None): use this root path instead of vhost_root's one
+            Used to add default site pages to external sites
+        @param _parent(Resource, None): _parent page. Do not set yourself, this is for
+            internal use only
+        @param _path(list(unicode), None): current path. Do not set yourself, this is for
+            internal use only
+        @param _extra_pages(boolean): set to True when extra pages are used (i.e.
+            root_path is set). Do not set yourself, this is for internal use only
+        """
+        if _path is None:
+            _path = []
+        if _parent is None:
+            if root_path is None:
+                root_dir = os.path.join(vhost_root.site_path, C.PAGES_DIR)
+            else:
+                root_dir = os.path.join(root_path, C.PAGES_DIR)
+                _extra_pages = True
+            _parent = vhost_root
         else:
-            root_dir = parent.root_dir
+            root_dir = _parent.root_dir
         for d in os.listdir(root_dir):
             dir_path = os.path.join(root_dir, d)
             if not os.path.isdir(dir_path):
                 continue
+            if _extra_pages and d in _parent.children:
+                log.debug(_(u"[{host_name}] {path} is already present, ignoring it").format(
+                    host_name=vhost_root.host_name, path=u'/'.join(_path+[d])))
+                continue
             meta_path = os.path.join(dir_path, C.PAGES_META_FILE)
             if os.path.isfile(meta_path):
                 page_data = {}
-                new_path = path + [d]
+                new_path = _path + [d]
                 # we don't want to force the presence of __init__.py
                 # so we use execfile instead of import.
                 # TODO: when moved to Python 3, __init__.py is not mandatory anymore
                 #       so we can switch to import
                 execfile(meta_path, page_data)
-                resource = LiberviaPage(
-                    host,
-                    dir_path,
-                    u"/" + u"/".join(new_path),
-                    name=page_data.get("name"),
-                    redirect=page_data.get("redirect"),
-                    access=page_data.get("access"),
-                    dynamic=page_data.get("dynamic", False),
-                    parse_url=page_data.get("parse_url"),
-                    prepare_render=page_data.get("prepare_render"),
-                    render=page_data.get("render"),
-                    template=page_data.get("template"),
-                    on_data_post=page_data.get("on_data_post"),
-                    on_data=page_data.get("on_data"),
-                    on_signal=page_data.get("on_signal"),
-                    url_cache=page_data.get("url_cache", False),
-                )
-                parent.putChild(d, resource)
-                log.info(u"Added /{path} page".format(path=u"[...]/".join(new_path)))
+                try:
+                    resource = LiberviaPage(
+                        host=host,
+                        vhost_root=vhost_root,
+                        root_dir=dir_path,
+                        url=u"/" + u"/".join(new_path),
+                        name=page_data.get(u"name"),
+                        redirect=page_data.get(u"redirect"),
+                        access=page_data.get(u"access"),
+                        dynamic=page_data.get(u"dynamic", False),
+                        parse_url=page_data.get(u"parse_url"),
+                        prepare_render=page_data.get(u"prepare_render"),
+                        render=page_data.get(u"render"),
+                        template=page_data.get(u"template"),
+                        on_data_post=page_data.get(u"on_data_post"),
+                        on_data=page_data.get(u"on_data"),
+                        on_signal=page_data.get(u"on_signal"),
+                        url_cache=page_data.get(u"url_cache", False),
+                    )
+                except exceptions.ConflictError as e:
+                    if _extra_pages:
+                        # extra pages are discarded if there is already an existing page
+                        continue
+                    else:
+                        raise e
+                _parent.putChild(d, resource)
+                log_msg = (u"[{host_name}] Added /{path} page".format(
+                    host_name=vhost_root.host_name,
+                    path=u"[…]/".join(new_path)))
+                if _extra_pages:
+                    log.debug(log_msg)
+                else:
+                    log.info(log_msg)
                 if "uri_handlers" in page_data:
                     if not isinstance(page_data, dict):
                         log.error(_(u"uri_handlers must be a dict"))
@@ -295,50 +333,20 @@
                             if len(uri_tuple) != 2 or not isinstance(cb_name, basestring):
                                 log.error(_(u"invalid uri_tuple"))
                                 continue
-                            log.info(_(u"setting {}/{} URIs handler").format(*uri_tuple))
+                            if not _extra_pages:
+                                log.info(_(u"setting {}/{} URIs handler")
+                                         .format(*uri_tuple))
                             try:
                                 cb = page_data[cb_name]
                             except KeyError:
-                                log.error(
-                                    _(u"missing {name} method to handle {1}/{2}").format(
-                                        name=cb_name, *uri_tuple
-                                    )
-                                )
+                                log.error(_(u"missing {name} method to handle {1}/{2}")
+                                          .format(name=cb_name, *uri_tuple))
                                 continue
                             else:
                                 resource.registerURI(uri_tuple, cb)
 
-                LiberviaPage.importPages(host, resource, new_path)
-
-    @classmethod
-    def setMenu(cls, menus):
-        main_menu = []
-        for menu in menus:
-            if not menu:
-                msg = _(u"menu item can't be empty")
-                log.error(msg)
-                raise ValueError(msg)
-            elif isinstance(menu, list):
-                if len(menu) != 2:
-                    msg = _(
-                        u"menu item as list must be in the form [page_name, absolue URL]"
-                    )
-                    log.error(msg)
-                    raise ValueError(msg)
-                page_name, url = menu
-            else:
-                page_name = menu
-                try:
-                    url = cls.getPageByName(page_name).url
-                except KeyError as e:
-                    log.error(
-                        _(
-                            u"Can'find a named page ({msg}), please check menu_json in configuration."
-                        ).format(msg=e)
-                    )
-                    raise e
-            main_menu.append((page_name, url))
-        cls.main_menu = main_menu
+                LiberviaPage.importPages(
+                    host, vhost_root, _parent=resource, _path=new_path, _extra_pages=_extra_pages)
 
     def registerURI(self, uri_tuple, get_uri_cb):
         """register a URI handler
@@ -351,11 +359,8 @@
             can't handle this URL
         """
         if uri_tuple in self.uri_callbacks:
-            log.info(
-                _(u"{}/{} URIs are already handled, replacing by the new handler").format(
-                    *uri_tuple
-                )
-            )
+            log.info(_(u"{}/{} URIs are already handled, replacing by the new handler")
+                .format( *uri_tuple))
         self.uri_callbacks[uri_tuple] = (self, get_uri_cb)
 
     def registerSignal(self, request, signal, check_profile=True):
@@ -369,12 +374,13 @@
         signal handler will be removed when connection with dynamic page will be lost
         @param signal(unicode): name of the signal
             last arg of signal must be profile, as it will be checked to filter signals
-        @param check_profile(bool): if True, signal profile (which MUST be last arg) will be
-            checked against session profile.
-            /!\ if False, profile will not be checked/filtered, be sure to know what you are doing
-                if you unset this option /!\
+        @param check_profile(bool): if True, signal profile (which MUST be last arg)
+            will be checked against session profile.
+            /!\ if False, profile will not be checked/filtered, be sure to know what you
+                are doing if you unset this option /!\
         """
-        # FIXME: add a timeout, if socket is not opened before it, signal handler must be removed
+        # FIXME: add a timeout; if socket is not opened before it, signal handler
+        #        must be removed
         if not self.dynamic:
             log.error(_(u"You can't register signal if page is not dynamic"))
             return
@@ -385,41 +391,11 @@
         )
         request._signals_registered.append(signal)
 
-    @classmethod
-    def getPagePathFromURI(cls, uri):
-        """Retrieve page URL from xmpp: URI
+    def getPageByName(self, name):
+        return self.vhost_root.getPageByName(name)
 
-        @param uri(unicode): URI with a xmpp: scheme
-        @return (unicode,None): absolute path (starting from root "/") to page handling the URI
-            None is returned if no page has been registered for this URI
-        """
-        uri_data = common_uri.parseXMPPUri(uri)
-        try:
-            page, cb = cls.uri_callbacks[uri_data["type"], uri_data["sub_type"]]
-        except KeyError:
-            url = None
-        else:
-            url = cb(page, uri_data)
-        if url is None:
-            # no handler found
-            # we try to find a more generic one
-            try:
-                page, cb = cls.uri_callbacks[uri_data["type"], None]
-            except KeyError:
-                pass
-            else:
-                url = cb(page, uri_data)
-        return url
-
-    @classmethod
-    def getPageByName(cls, name):
-        """retrieve page instance from its name
-
-        @param name(unicode): name of the page
-        @return (LiberviaPage): page instance
-        @raise KeyError: the page doesn't exist
-        """
-        return cls.named_pages[name]
+    def getPagePathFromURI(self, uri):
+        return self.vhost.getPagePathFromURI(uri)
 
     def getPageRedirectURL(self, request, page_name=u"login", url=None):
         """generate URL for a page with redirect_url parameter set
@@ -550,6 +526,7 @@
     def getURLByNames(self, named_path):
         """retrieve URL from pages names and arguments
 
+        @param request(server.Request): request linked to the session
         @param named_path(list[tuple[unicode, list[unicode]]]): path to the page as a list
             of tuples of 2 items:
                 - first item is page name
@@ -570,13 +547,14 @@
                 path.append(sub_path)
                 if page_args:
                     path.extend([quote(a) for a in page_args])
-        return self.host.checkRedirection(u"/".join(path))
+        return self.host.checkRedirection(self.vhost_root, u"/".join(path))
 
     def getURLByPath(self, *args):
         """generate URL by path
 
         this method as a similar effect as getURLByNames, but it is more readable
         by using SubPage to get pages instead of using tuples
+        @param request(server.Request): request linked to the session
         @param *args: path element:
             - if unicode, will be used as argument
             - if util.SubPage instance, must be the name of a subpage
@@ -607,7 +585,7 @@
             else:
                 path, current_page = current_page.getSubPageByName(args.pop(0))
                 arguments = [path]
-        return self.host.checkRedirection(u"/".join(url_elts))
+        return self.host.checkRedirection(self.vhost_root, u"/".join(url_elts))
 
     def getChildWithDefault(self, path, request):
         # we handle children ourselves
@@ -655,15 +633,18 @@
         Arguments will be put in request data.
         Missing arguments will have None value
         @param names(list[unicode]): list of arguments to get
-        @param min_args(int): if less than min_args are found, PageError is used with C.HTTP_BAD_REQUEST
-            use 0 to ignore
+        @param min_args(int): if less than min_args are found, PageError is used with
+            C.HTTP_BAD_REQUEST
+            Use 0 to ignore
         @param **kwargs: special value or optional callback to use for arguments
             names of the arguments must correspond to those in names
             special values may be:
                 - '': use empty string instead of None when no value is specified
                 - '@': if value of argument is empty or '@', empty string will be used
-                - 'jid': value must be converted to jid.JID if it exists, else empty string is used
-                - '@jid': if value of arguments is empty or '@', empty string will be used, else it will be converted to jid
+                - 'jid': value must be converted to jid.JID if it exists, else empty
+                    string is used
+                - '@jid': if value of arguments is empty or '@', empty string will be
+                    used, else it will be converted to jid
         """
         data = self.getRData(request)
 
@@ -688,11 +669,8 @@
 
         values_count = idx + 1
         if values_count < min_args:
-            log.warning(
-                _(
-                    u"Missing arguments in URL (got {count}, expected at least {min_args})"
-                ).format(count=values_count, min_args=min_args)
-            )
+            log.warning(_(u"Missing arguments in URL (got {count}, expected at least "
+                          u"{min_args})").format(count=values_count, min_args=min_args))
             self.pageError(request, C.HTTP_BAD_REQUEST)
 
         for name in names[values_count:]:
@@ -767,17 +745,16 @@
                     short = kwargs["short"]
                     node = self.host.ns_map[short]
                 except KeyError:
-                    log.warning(
-                        _(
-                            u'Can\'t use cache for empty node without namespace set, please ensure to set "short" and that it is registered'
-                        )
-                    )
+                    log.warning(_(u'Can\'t use cache for empty node without namespace '
+                                  u'set, please ensure to set "short" and that it is '
+                                  u'registered'))
                     return
             if profile != C.SERVICE_PROFILE:
                 #  only service profile is cache for now
                 return
             try:
-                cache = self.cache[profile][cache_type][service][node][request.uri][self]
+                cache = (self.cache[profile][cache_type][service][node]
+                         [self.vhost_root][request.uri][self])
             except KeyError:
                 # no cache yet, let's subscribe to the pubsub node
                 d1 = self.host.bridgeCall(
@@ -787,7 +764,8 @@
                 d1.addErrback(self.checkCacheSubscribeEb, service, node)
                 d2 = self.host.bridgeCall("psNodeWatchAdd", service.full(), node, profile)
                 d2.addErrback(self.psNodeWatchAddEb, service, node)
-                self._do_cache = [self, profile, cache_type, service, node, request.uri]
+                self._do_cache = [self, profile, cache_type, service, node,
+                                  self.vhost_root, request.uri]
                 #  we don't return the Deferreds as it is not needed to wait for
                 # the subscription to continue with page rendering
                 return
@@ -802,7 +780,7 @@
         request.finish()
         raise failure.Failure(exceptions.CancelError(u"cache is used"))
 
-    def _cacheURL(self, dummy, request, profile):
+    def _cacheURL(self, __, request, profile):
         self.cached_urls.setdefault(profile, {})[request.uri] = CacheURL(request)
 
     @classmethod
@@ -811,29 +789,20 @@
         try:
             cache = cls.cache[profile][C.CACHE_PUBSUB][jid.JID(service)][node]
         except KeyError:
-            log.info(
-                _(
-                    u"Removing subscription for {service}/{node}: "
-                    u"the page is not cached"
-                ).format(service=service, node=node)
-            )
+            log.info(_(
+                u"Removing subscription for {service}/{node}: "
+                u"the page is not cached").format(service=service, node=node))
             d1 = host.bridgeCall("psUnsubscribe", service, node, profile)
             d1.addErrback(
                 lambda failure_: log.warning(
                     _(u"Can't unsubscribe from {service}/{node}: {msg}").format(
-                        service=service, node=node, msg=failure_
-                    )
-                )
-            )
+                        service=service, node=node, msg=failure_)))
             d2 = host.bridgeCall("psNodeWatchAdd", service, node, profile)
             # TODO: check why the page is not in cache, remove subscription?
             d2.addErrback(
                 lambda failure_: log.warning(
                     _(u"Can't remove watch for {service}/{node}: {msg}").format(
-                        service=service, node=node, msg=failure_
-                    )
-                )
-            )
+                        service=service, node=node, msg=failure_)))
         else:
             cache.clear()
 
@@ -889,11 +858,8 @@
             try:
                 del LiberviaPage.signals_handlers[signal][id(request)]
             except KeyError:
-                log.error(
-                    _(
-                        u"Can't find signal handler for [{signal}], this should not happen"
-                    ).format(signal=signal)
-                )
+                log.error(_(u"Can't find signal handler for [{signal}], this should not "
+                            u"happen").format(signal=signal))
             else:
                 log.debug(_(u"Removed signal handler"))
 
@@ -952,21 +918,22 @@
                 - following element are subpages path
             e.g.: "blog" redirect to page named "blog"
                   "blog/atom.xml" redirect to atom.xml subpage of "blog"
-                  "/common/blog/atom.xml" redirect to the page at the fiven full path
+                  "/common/blog/atom.xml" redirect to the page at the given full path
         @param request(server.Request): current HTTP request
-        @param skip_parse_url(bool): if True, parse_url method on redirect page will be skipped
+        @param skip_parse_url(bool): if True, parse_url method on redirect page will be
+            skipped
         @param path_args(list[unicode], None): path arguments to use in redirected page
         @raise KeyError: there is no known page with this name
         """
         # FIXME: render non LiberviaPage resources
         path = page_path.rstrip(u"/").split(u"/")
         if not path[0]:
-            redirect_page = self.host.root
+            redirect_page = self.vhost_root
         else:
             redirect_page = self.named_pages[path[0]]
 
         for subpage in path[1:]:
-            if redirect_page is self.host.root:
+            if redirect_page is self.vhost_root:
                 redirect_page = redirect_page.children[subpage]
             else:
                 redirect_page = redirect_page.original.children[subpage]
@@ -995,6 +962,8 @@
             request.finish()
         else:
             template = u"error/" + unicode(code) + ".html"
+            if self.vhost_root.site_name:
+                request.template_data[u'site'] = self.vhost_root.site_name
 
             rendered = self.host.renderer.render(
                 template,
@@ -1016,18 +985,15 @@
             cache = reduce(lambda d, k: d.setdefault(k, {}), self._do_cache, self.cache)
             page_cache = cache[redirected_page] = CachePage(data_encoded)
             self._setCacheHeaders(request, page_cache)
-            log.debug(
-                _(u"{page} put in cache for [{profile}]").format(
-                    page=self, profile=self._do_cache[0]
-                )
-            )
+            log.debug(_(u"{page} put in cache for [{profile}]")
+                .format( page=self, profile=self._do_cache[0]))
             self._do_cache = None
             self._checkCacheHeaders(request, page_cache)
 
         request.write(data_encoded)
         request.finish()
 
-    def _subpagesHandler(self, dummy, request):
+    def _subpagesHandler(self, __, request):
         """render subpage if suitable
 
         this method checks if there is still an unmanaged part of the path
@@ -1045,7 +1011,7 @@
                 child.render(request)
                 raise failure.Failure(exceptions.CancelError(u"subpage page is used"))
 
-    def _prepare_dynamic(self, dummy, request):
+    def _prepare_dynamic(self, __, request):
         # we need to activate dynamic page
         # we set data for template, and create/register token
         socket_token = unicode(uuid.uuid4())
@@ -1060,27 +1026,28 @@
         # we will cache registered signals until socket is opened
         request._signals_cache = []
 
-    def _prepare_render(self, dummy, request):
+    def _prepare_render(self, __, request):
         return defer.maybeDeferred(self.prepare_render, self, request)
 
-    def _render_method(self, dummy, request):
+    def _render_method(self, __, request):
         return defer.maybeDeferred(self.render_method, self, request)
 
-    def _render_template(self, dummy, request):
+    def _render_template(self, __, request):
         template_data = request.template_data
 
         # if confirm variable is set in case of successfuly data post
         session_data = self.host.getSessionData(request, session_iface.ISATSession)
         if session_data.popPageFlag(self, C.FLAG_CONFIRM):
             template_data[u"confirm"] = True
+        if self.vhost_root.site_name:
+            template_data[u'site'] = self.vhost_root.site_name
 
         return self.host.renderer.render(
             self.template,
             media_path="/" + C.MEDIA_DIR,
             cache_path=session_data.cache_dir,
-            main_menu=LiberviaPage.main_menu,
-            **template_data
-        )
+            main_menu=self.main_menu,
+            **template_data)
 
     def _renderEb(self, failure_, request):
         """don't raise error on CancelError"""
@@ -1088,11 +1055,8 @@
 
     def _internalError(self, failure_, request):
         """called if an error is not catched"""
-        log.error(
-            _(u"Uncatched error for HTTP request on {url}: {msg}").format(
-                url=request.URLPath(), msg=failure_
-            )
-        )
+        log.error(_(u"Uncatched error for HTTP request on {url}: {msg}")
+            .format( url=request.URLPath(), msg=failure_))
         self.pageError(request, C.HTTP_INTERNAL_ERROR)
 
     def _on_data_post_redirect(self, ret, request):
@@ -1100,9 +1064,12 @@
 
         This will do a Post/Redirect/Get pattern.
         this method redirect to the same page or to request.data['post_redirect_page']
-        post_redirect_page can be either a page or a tuple with page as first item, then a list of unicode arguments to append to the url.
-        if post_redirect_page is not used, initial request.uri (i.e. the same page as where the data have been posted) will be used for redirection.
-        HTTP status code "See Other" (303) is used as it is the recommanded code in this case.
+        post_redirect_page can be either a page or a tuple with page as first item, then
+        a list of unicode arguments to append to the url.
+        if post_redirect_page is not used, initial request.uri (i.e. the same page as
+        where the data have been posted) will be used for redirection.
+        HTTP status code "See Other" (303) is used as it is the recommanded code in
+        this case.
         @param ret(None, unicode, iterable): on_data_post return value
             see LiberviaPage.__init__ on_data_post docstring
         """
@@ -1137,7 +1104,7 @@
         request.finish()
         raise failure.Failure(exceptions.CancelError(u"Post/Redirect/Get is used"))
 
-    def _on_data_post(self, dummy, request):
+    def _on_data_post(self, __, request):
         csrf_token = self.host.getSessionData(
             request, session_iface.ISATSession
         ).csrf_token
@@ -1165,10 +1132,12 @@
             iterable to get more than one
         @param multiple(bool): True if multiple values are possible/expected
             if False, the first value is returned
-        @return (iterator[unicode], list[iterator[unicode], unicode, list[unicode]): values received for this(these) key(s)
+        @return (iterator[unicode], list[iterator[unicode], unicode, list[unicode]):
+            values received for this(these) key(s)
         @raise KeyError: one specific key has been requested, and it is missing
         """
-        #  FIXME: request.args is already unquoting the value, it seems we are doing double unquote
+        #  FIXME: request.args is already unquoting the value, it seems we are doing
+        #         double unquote
         if isinstance(keys, basestring):
             keys = [keys]
             get_first = True
@@ -1222,7 +1191,8 @@
         """helper method to get request data dict
 
         this dictionnary if for the request only, it is not saved in session
-        It is mainly used to pass data between pages/methods called during request workflow
+        It is mainly used to pass data between pages/methods called during request
+        workflow
         @return (dict): request data
         """
         try:
@@ -1266,12 +1236,14 @@
                 _(u"renderPartial must only be used with dynamic pages")
             )
         session_data = self.host.getSessionData(request, session_iface.ISATSession)
+        if self.vhost_root.site_name:
+            template_data[u'site'] = self.vhost_root.site_name
 
         return self.host.renderer.render(
             template,
             media_path="/" + C.MEDIA_DIR,
             cache_path=session_data.cache_dir,
-            main_menu=LiberviaPage.main_menu,
+            main_menu=self.main_menu,
             **template_data
         )
 
@@ -1319,7 +1291,7 @@
 
         if self.redirect is not None:
             d.addCallback(
-                lambda dummy: self.pageRedirect(
+                lambda __: self.pageRedirect(
                     self.redirect, request, skip_parse_url=False
                 )
             )
@@ -1344,13 +1316,13 @@
 
         if request.method not in (C.HTTP_METHOD_GET, C.HTTP_METHOD_POST):
             # only HTTP GET and POST are handled so far
-            d.addCallback(lambda dummy: self.pageError(request, C.HTTP_BAD_REQUEST))
+            d.addCallback(lambda __: self.pageError(request, C.HTTP_BAD_REQUEST))
 
         if request.method == C.HTTP_METHOD_POST:
             if self.on_data_post is None:
                 # if we don't have on_data_post, the page was not expecting POST
                 # so we return an error
-                d.addCallback(lambda dummy: self.pageError(request, C.HTTP_BAD_REQUEST))
+                d.addCallback(lambda __: self.pageError(request, C.HTTP_BAD_REQUEST))
             else:
                 d.addCallback(self._on_data_post, request)
             # by default, POST follow normal behaviour after on_data_post is called
--- a/libervia/server/server.py	Sun Sep 09 21:12:22 2018 +0200
+++ b/libervia/server/server.py	Fri Sep 14 21:41:28 2018 +0200
@@ -24,6 +24,7 @@
 from twisted.web import resource as web_resource
 from twisted.web import util as web_util
 from twisted.web import http
+from twisted.web import vhost
 from twisted.python.components import registerAdapter
 from twisted.python import failure
 from twisted.words.protocols.jabber import jid
@@ -33,7 +34,6 @@
 
 from sat.core.log import getLogger
 
-log = getLogger(__name__)
 from sat_frontends.bridge.dbus_bridge import (
     Bridge,
     BridgeExceptionNoService,
@@ -44,6 +44,7 @@
 from sat.tools import utils
 from sat.tools.common import regex
 from sat.tools.common import template
+from sat.tools.common import uri as common_uri
 
 import re
 import glob
@@ -72,8 +73,11 @@
 from libervia.server.blog import MicroBlog
 from libervia.server import session_iface
 
+log = getLogger(__name__)
 
-# following value are set from twisted.plugins.libervia_server initialise (see the comment there)
+
+# following value are set from twisted.plugins.libervia_server initialise
+# (see the comment there)
 DATA_DIR_DEFAULT = OPT_PARAMETERS_BOTH = OPT_PARAMETERS_CFG = coerceDataDir = None
 
 
@@ -100,7 +104,7 @@
 
 
 class ProtectedFile(static.File):
-    """A static.File class which doens't show directory listing"""
+    """A static.File class which doesn't show directory listing"""
 
     def directoryListing(self):
         return web_resource.NoResource()
@@ -112,28 +116,46 @@
     handle redirections declared in sat.conf
     """
 
+    def __init__(self, host, host_name, site_name, site_path, *args, **kwargs):
+        ProtectedFile.__init__(self, *args, **kwargs)
+        self.host = host
+        self.host_name = host_name
+        self.site_name = site_name
+        self.site_path = site_path
+        self.named_pages = {}
+        self.uri_callbacks = {}
+        self.pages_redirects = {}
+        self.cached_urls = {}
+        self.main_menu = None
+
+    def __unicode__(self):
+        return (u"Root resource for {host_name} using {site_name} at {site_path} and "
+                u"deserving files at {path}".format(
+                host_name=self.host_name, site_name=self.site_name,
+                site_path=self.site_path, path=self.path))
+
+    def __str__(self):
+        return self.__unicode__.encode('utf-8')
+
     def _initRedirections(self, options):
+        url_redirections = options["url_redirections_dict"]
+
+        url_redirections = url_redirections.get(self.site_name, {})
+
         ## redirections
         self.redirections = {}
         self.inv_redirections = {}  # new URL to old URL map
 
-        if options["url_redirections_dict"] and not options["url_redirections_profile"]:
-            # FIXME: url_redirections_profile should not be needed. It is currently used to
-            #        redirect to an URL which associate the profile with the service, but this
-            #        is not clean, and service should be explicitly specified
-            raise ValueError(
-                u"url_redirections_profile need to be filled if you want to use url_redirections_dict"
-            )
-
-        for old, new_data in options["url_redirections_dict"].iteritems():
+        for old, new_data in url_redirections.iteritems():
             # new_data can be a dictionary or a unicode url
             if isinstance(new_data, dict):
-                # new_data dict must contain either "url", "page" or "path" key (exclusive)
+                # new_data dict must contain either "url", "page" or "path" key
+                # (exclusive)
                 # if "path" is used, a file url is constructed with it
                 if len({"path", "url", "page"}.intersection(new_data.keys())) != 1:
                     raise ValueError(
-                        u'You must have one and only one of "url", "page" or "path" key in your url_redirections_dict data'
-                    )
+                        u'You must have one and only one of "url", "page" or "path" key '
+                        u'in your url_redirections_dict data')
                 if "url" in new_data:
                     new = new_data["url"]
                 elif "page" in new_data:
@@ -142,29 +164,23 @@
                     new.setdefault("path_args", [])
                     if not isinstance(new["path_args"], list):
                         log.error(
-                            _(
-                                u'"path_args" in redirection of {old} must be a list. Ignoring the redirection'.format(
-                                    old=old
-                                )
-                            )
-                        )
+                            _(u'"path_args" in redirection of {old} must be a list. '
+                              u'Ignoring the redirection'.format(old=old)))
                         continue
                     new.setdefault("query_args", {})
                     if not isinstance(new["query_args"], dict):
                         log.error(
                             _(
-                                u'"query_args" in redirection of {old} must be a dictionary. Ignoring the redirection'.format(
-                                    old=old
-                                )
-                            )
-                        )
+                                u'"query_args" in redirection of {old} must be a '
+                                u'dictionary. Ignoring the redirection'.format(old=old)))
                         continue
                     new["path_args"] = [quote(a) for a in new["path_args"]]
-                    # we keep an inversed dict of page redirection (page/path_args => redirecting URL)
-                    # so getURL can return the redirecting URL if the same arguments are used
-                    # making the URL consistent
+                    # we keep an inversed dict of page redirection
+                    # (page/path_args => redirecting URL)
+                    # so getURL can return the redirecting URL if the same arguments
+                    # are used # making the URL consistent
                     args_hash = tuple(new["path_args"])
-                    LiberviaPage.pages_redirects.setdefault(new_data["page"], {})[
+                    self.pages_redirects.setdefault(new_data["page"], {})[
                         args_hash
                     ] = old
 
@@ -213,7 +229,7 @@
                         )
                 else:
                     if new[u"type"] == u"page":
-                        page = LiberviaPage.getPageByName(new[u"page"])
+                        page = self.getPageByName(new[u"page"])
                         url = page.getURL(*new.get(u"path_args", []))
                         self.inv_redirections[url] = old
                 continue
@@ -223,13 +239,11 @@
 
             # we handle the known URL schemes
             if new_url.scheme == "xmpp":
-                location = LiberviaPage.getPagePathFromURI(new)
+                location = self.getPagePathFromURI(new)
                 if location is None:
                     log.warning(
-                        _(
-                            u"ignoring redirection, no page found to handle this URI: {uri}"
-                        ).format(uri=new)
-                    )
+                        _(u"ignoring redirection, no page found to handle this URI: "
+                          u"{uri}").format(uri=new))
                     continue
                 request_data = self._getRequestData(location)
                 if old:
@@ -239,10 +253,9 @@
                 # direct redirection
                 if new_url.netloc:
                     raise NotImplementedError(
-                        u"netloc ({netloc}) is not implemented yet for url_redirections_dict, it is not possible to redirect to an external website".format(
-                            netloc=new_url.netloc
-                        )
-                    )
+                        u"netloc ({netloc}) is not implemented yet for "
+                        u"url_redirections_dict, it is not possible to redirect to an "
+                        u"external website".format(netloc=new_url.netloc))
                 location = urlparse.urlunsplit(
                     ("", "", new_url.path, new_url.query, new_url.fragment)
                 ).decode("utf-8")
@@ -254,17 +267,17 @@
                 # file or directory
                 if new_url.netloc:
                     raise NotImplementedError(
-                        u"netloc ({netloc}) is not implemented for url redirection to file system, it is not possible to redirect to an external host".format(
-                            netloc=new_url.netloc
-                        )
-                    )
+                        u"netloc ({netloc}) is not implemented for url redirection to "
+                        u"file system, it is not possible to redirect to an external "
+                        "host".format(
+                            netloc=new_url.netloc))
                 path = urllib.unquote(new_url.path)
                 if not os.path.isabs(path):
                     raise ValueError(
-                        u"file redirection must have an absolute path: e.g. file:/path/to/my/file"
-                    )
+                        u"file redirection must have an absolute path: e.g. "
+                        u"file:/path/to/my/file")
                 # for file redirection, we directly put child here
-                segments, dummy, last_segment = old.rpartition("/")
+                segments, __, last_segment = old.rpartition("/")
                 url_segments = segments.split("/") if segments else []
                 current = self
                 for segment in url_segments:
@@ -275,11 +288,10 @@
                     ProtectedFile if new_data.get("protected", True) else static.File
                 )
                 current.putChild(last_segment, resource_class(path))
-                log.info(
-                    u"Added redirection from /{old} to file system path {path}".format(
-                        old=old.decode("utf-8"), path=path.decode("utf-8")
-                    )
-                )
+                log.info(u"[{host_name}] Added redirection from /{old} to file system "
+                         u"path {path}".format(host_name=self.host_name,
+                                               old=old.decode("utf-8"),
+                                               path=path.decode("utf-8")))
                 continue  # we don't want to use redirection system, so we continue here
 
             else:
@@ -291,20 +303,41 @@
 
             self.redirections[old] = request_data
             if not old:
-                log.info(
-                    _(u"Root URL redirected to {uri}").format(
-                        uri=request_data[1].decode("utf-8")
-                    )
-                )
-
-        # no need to keep url_redirections*, they will not be used anymore
-        del options["url_redirections_dict"]
-        del options["url_redirections_profile"]
+                log.info(_(u"[{host_name}] Root URL redirected to {uri}")
+                    .format(host_name=self.host_name,
+                            uri=request_data[1].decode("utf-8")))
 
         # the default root URL, if not redirected
         if not "" in self.redirections:
             self.redirections[""] = self._getRequestData(C.LIBERVIA_MAIN_PAGE)
 
+    def _setMenu(self, menus):
+        menus = menus.get(self.site_name, [])
+        main_menu = []
+        for menu in menus:
+            if not menu:
+                msg = _(u"menu item can't be empty")
+                log.error(msg)
+                raise ValueError(msg)
+            elif isinstance(menu, list):
+                if len(menu) != 2:
+                    msg = _(
+                        u"menu item as list must be in the form [page_name, absolue URL]"
+                    )
+                    log.error(msg)
+                    raise ValueError(msg)
+                page_name, url = menu
+            else:
+                page_name = menu
+                try:
+                    url = self.getPageByName(page_name).url
+                except KeyError as e:
+                    log.error(_(u"Can'find a named page ({msg}), please check "
+                                u"menu_json in configuration.").format(msg=e))
+                    raise e
+            main_menu.append((page_name, url))
+        self.main_menu = main_menu
+
     def _normalizeURL(self, url, lower=True):
         """Return URL normalized for self.redirections dict
 
@@ -362,14 +395,12 @@
             pass
         else:
             try:
-                dummy, uri, dummy, dummy = request_data
+                __, uri, __, __ = request_data
             except ValueError:
                 uri = u""
-            log.error(
-                D_(
-                    u"recursive redirection, please fix this URL:\n{old} ==> {new}"
-                ).format(old=request.uri.decode("utf-8"), new=uri.decode("utf-8"))
-            )
+            log.error(D_( u"recursive redirection, please fix this URL:\n"
+                          u"{old} ==> {new}").format(
+                          old=request.uri.decode("utf-8"), new=uri.decode("utf-8")))
             return web_resource.NoResource()
 
         request._redirected = True  # here to avoid recursive redirections
@@ -377,7 +408,7 @@
         if isinstance(request_data, dict):
             if request_data["type"] == "page":
                 try:
-                    page = LiberviaPage.getPageByName(request_data["page"])
+                    page = self.getPageByName(request_data["page"])
                 except KeyError:
                     log.error(
                         _(
@@ -413,6 +444,41 @@
         # we start again to look for a child with the new url
         return self.getChildWithDefault(path_list[0], request)
 
+    def getPageByName(self, name):
+        """Retrieve page instance from its name
+
+        @param name(unicode): name of the page
+        @return (LiberviaPage): page instance
+        @raise KeyError: the page doesn't exist
+        """
+        return self.named_pages[name]
+
+    def getPagePathFromURI(self, uri):
+        """Retrieve page URL from xmpp: URI
+
+        @param uri(unicode): URI with a xmpp: scheme
+        @return (unicode,None): absolute path (starting from root "/") to page handling
+            the URI.
+            None is returned if no page has been registered for this URI
+        """
+        uri_data = common_uri.parseXMPPUri(uri)
+        try:
+            page, cb = self.uri_callbacks[uri_data["type"], uri_data["sub_type"]]
+        except KeyError:
+            url = None
+        else:
+            url = cb(page, uri_data)
+        if url is None:
+            # no handler found
+            # we try to find a more generic one
+            try:
+                page, cb = self.uri_callbacks[uri_data["type"], None]
+            except KeyError:
+                pass
+            else:
+                url = cb(page, uri_data)
+        return url
+
     def getChildWithDefault(self, name, request):
         # XXX: this method is overriden only for root url
         #      which is the only ones who need to be handled before other children
@@ -436,6 +502,15 @@
 
         return resource
 
+    def putChild(self, path, resource):
+        """Add a child to the root resource"""
+        if not isinstance(resource, web_resource.EncodingResourceWrapper):
+            # FIXME: check that no information is leaked (c.f. https://twistedmatrix.com/documents/current/web/howto/using-twistedweb.html#request-encoders)
+            resource = web_resource.EncodingResourceWrapper(
+                resource, [server.GzipEncoderFactory()])
+
+        super(LiberviaRootResource, self).putChild(path, resource)
+
     def createSimilarFile(self, path):
         # XXX: this method need to be overriden to avoid recreating a LiberviaRootResource
 
@@ -565,7 +640,8 @@
     #     @param notify (bool): True if notification is required
     #     """
     #     profile = session_iface.ISATSession(self.session).profile
-    #     return self.asyncBridgeCall("psRetractItem", service, node, item, notify, profile)
+    #     return self.asyncBridgeCall("psRetractItem", service, node, item, notify,
+    #                                 profile)
 
     # def jsonrpc_psRetractItems(self, service, node, items, notify):
     #     """Delete a whole node
@@ -576,7 +652,8 @@
     #     @param notify (bool): True if notification is required
     #     """
     #     profile = session_iface.ISATSession(self.session).profile
-    #     return self.asyncBridgeCall("psRetractItems", service, node, items, notify, profile)
+    #     return self.asyncBridgeCall("psRetractItems", service, node, items, notify,
+    #                                 profile)
 
     ## microblogging ##
 
@@ -606,7 +683,8 @@
 
         @param service_jid (unicode): pubsub service, usually publisher jid
         @param node(unicode): mblogs node, or empty string to get the defaut one
-        @param max_items (int): maximum number of item to get or C.NO_LIMIT to get everything
+        @param max_items (int): maximum number of item to get or C.NO_LIMIT to get
+            everything
         @param item_ids (list[unicode]): list of item IDs
         @param rsm (dict): TODO
         @return: a deferred couple with the list of items and metadatas.
@@ -620,8 +698,10 @@
         """Get many blog nodes at once
 
         @param publishers_type (unicode): one of "ALL", "GROUP", "JID"
-        @param publishers (tuple(unicode)): tuple of publishers (empty list for all, list of groups or list of jids)
-        @param max_items (int): maximum number of item to get or C.NO_LIMIT to get everything
+        @param publishers (tuple(unicode)): tuple of publishers (empty list for all,
+            list of groups or list of jids)
+        @param max_items (int): maximum number of item to get or C.NO_LIMIT to get
+            everything
         @param extra (dict): TODO
         @return (str): RT Deferred session id
         """
@@ -649,8 +729,10 @@
     ):
         """Helper method to get the microblogs and their comments in one shot
 
-        @param publishers_type (str): type of the list of publishers (one of "GROUP" or "JID" or "ALL")
-        @param publishers (list): list of publishers, according to publishers_type (list of groups or list of jids)
+        @param publishers_type (str): type of the list of publishers (one of "GROUP" or
+            "JID" or "ALL")
+        @param publishers (list): list of publishers, according to publishers_type
+            (list of groups or list of jids)
         @param max_items (int): optional limit on the number of retrieved items.
         @param max_comments (int): maximum number of comments to retrieve
         @param rsm_dict (dict): RSM data for initial items only
@@ -696,11 +778,13 @@
     #         if type_ == "PUBLIC":
     #             #This text if for the public microblog
     #             log.debug("sending public blog")
-    #             return self.sat_host.bridge.sendGroupBlog("PUBLIC", (), text, extra, profile)
+    #             return self.sat_host.bridge.sendGroupBlog("PUBLIC", (), text, extra,
+    #                                                       profile)
     #         else:
     #             log.debug("sending group blog")
     #             dest = dest if isinstance(dest, list) else [dest]
-    #             return self.sat_host.bridge.sendGroupBlog("GROUP", dest, text, extra, profile)
+    #             return self.sat_host.bridge.sendGroupBlog("GROUP", dest, text, extra,
+    #                                                       profile)
     #     else:
     #         raise Exception("Invalid data")
 
@@ -710,7 +794,8 @@
     #     @param comments: comments node identifier (for main item) or False
     #     """
     #     profile = session_iface.ISATSession(self.session).profile
-    #     return self.sat_host.bridge.deleteGroupBlog(pub_data, comments if comments else '', profile)
+    #     return self.sat_host.bridge.deleteGroupBlog(pub_data, comments if comments
+    #         else '', profile)
 
     # def jsonrpc_updateMblog(self, pub_data, comments, message, extra={}):
     #     """Modify a microblog node
@@ -718,13 +803,15 @@
     #     @param comments: comments node identifier (for main item) or False
     #     @param message: new message
     #     @param extra: dict which option name as key, which can be:
-    #         - allow_comments: True to accept an other level of comments, False else (default: False)
+    #         - allow_comments: True to accept an other level of comments, False else
+    #               (default: False)
     #         - rich: if present, contain rich text in currently selected syntax
     #     """
     #     profile = session_iface.ISATSession(self.session).profile
     #     if comments:
     #         extra['allow_comments'] = 'True'
-    #     return self.sat_host.bridge.updateGroupBlog(pub_data, comments if comments else '', message, extra, profile)
+    #     return self.sat_host.bridge.updateGroupBlog(pub_data, comments if comments
+    #         else '', message, extra, profile)
 
     # def jsonrpc_sendMblogComment(self, node, text, extra={}):
     #     """ Send microblog message
@@ -800,20 +887,18 @@
             and jid.JID(to_jid).userhost() != sat_jid.userhost()
         ):
             log.error(
-                u"Trying to get history from a different jid (given (browser): {}, real (backend): {}), maybe a hack attempt ?".format(
-                    from_jid, sat_jid
-                )
-            )
+                u"Trying to get history from a different jid (given (browser): {}, real "
+                u"(backend): {}), maybe a hack attempt ?".format( from_jid, sat_jid))
             return {}
         d = self.asyncBridgeCall(
-            "historyGet", from_jid, to_jid, size, between, search, profile
-        )
+            "historyGet", from_jid, to_jid, size, between, search, profile)
 
         def show(result_dbus):
             result = []
             for line in result_dbus:
-                # XXX: we have to do this stupid thing because Python D-Bus use its own types instead of standard types
-                #     and txJsonRPC doesn't accept D-Bus types, resulting in a empty query
+                # XXX: we have to do this stupid thing because Python D-Bus use its own
+                #      types instead of standard types and txJsonRPC doesn't accept
+                #      D-Bus types, resulting in a empty query
                 uuid, timestamp, from_jid, to_jid, message, subject, mess_type, extra = (
                     line
                 )
@@ -987,7 +1072,8 @@
         """Return the parameter value for profile"""
         profile = session_iface.ISATSession(self.session).profile
         if category == "Connection":
-            # we need to manage the followings params here, else SECURITY_LIMIT would block them
+            # we need to manage the followings params here, else SECURITY_LIMIT would
+            # block them
             if param == "JabberID":
                 return self.asyncBridgeCall(
                     "asyncGetParamA", param, category, attribute, profile_key=profile
@@ -1011,8 +1097,9 @@
         )
 
     def jsonrpc_launchAction(self, callback_id, data):
-        # FIXME: any action can be launched, this can be a huge security issue if callback_id can be guessed
-        #       a security system with authorised callback_id must be implemented, similar to the one for authorised params
+        # FIXME: any action can be launched, this can be a huge security issue if
+        #        callback_id can be guessed a security system with authorised
+        #        callback_id must be implemented, similar to the one for authorised params
         profile = session_iface.ISATSession(self.session).profile
         d = self.asyncBridgeCall("launchAction", callback_id, data, profile)
         return d
@@ -1068,7 +1155,8 @@
 
         @param request (server.Request): the connection request
         @param profile (str): %(doc_profile)s
-        @param register_with_ext_jid (bool): True if we will try to register the profile with an external XMPP account credentials
+        @param register_with_ext_jid (bool): True if we will try to register the
+            profile with an external XMPP account credentials
         """
         dc = reactor.callLater(BRIDGE_TIMEOUT, self.purgeRequest, profile)
         self[profile] = (request, dc, register_with_ext_jid)
@@ -1117,7 +1205,8 @@
         Render method with some hacks:
            - if login is requested, try to login with form data
            - except login, every method is jsonrpc
-           - user doesn't need to be authentified for explicitely listed methods, but must be for all others
+           - user doesn't need to be authentified for explicitely listed methods,
+             but must be for all others
         """
         if request.postpath == ["login"]:
             return self.loginOrRegister(request)
@@ -1179,12 +1268,16 @@
 
         will write to request a constant indicating the state:
             - C.PROFILE_LOGGED: profile is connected
-            - C.PROFILE_LOGGED_EXT_JID: profile is connected and an external jid has been used
+            - C.PROFILE_LOGGED_EXT_JID: profile is connected and an external jid has
+                been used
             - C.SESSION_ACTIVE: session was already active
             - C.BAD_REQUEST: something is wrong in the request (bad arguments)
-            - C.PROFILE_AUTH_ERROR: either the profile (login) or the profile password is wrong
-            - C.XMPP_AUTH_ERROR: the profile is authenticated but the XMPP password is wrong
-            - C.ALREADY_WAITING: a request has already been submitted for this profil, C.PROFILE_LOGGED_EXT_JID)e
+            - C.PROFILE_AUTH_ERROR: either the profile (login) or the profile password
+                is wrong
+            - C.XMPP_AUTH_ERROR: the profile is authenticated but the XMPP password
+                is wrong
+            - C.ALREADY_WAITING: a request has already been submitted for this profile,
+                C.PROFILE_LOGGED_EXT_JID)
             - C.NOT_CONNECTED: connection has not been established
         the request will then be finished
         @param request: request of the register form
@@ -1258,8 +1351,9 @@
 
         @return (dict): metadata which can have the following keys:
             "plugged" (bool): True if a profile is already plugged
-            "warning" (unicode): a security warning message if plugged is False and if it make sense
-                this key may not be present
+            "warning" (unicode): a security warning message if plugged is False and if
+                it make sense.
+                This key may not be present.
             "allow_registration" (bool): True if registration is allowed
                 this key is only present if profile is unplugged
         @return: a couple (registered, message) with:
@@ -1349,7 +1443,8 @@
             else:
                 # the queue is empty, we delete the profile from queue
                 del self.queue[profile]
-        _session.lock()  # we don't want the session to expire as long as this connection is active
+        _session.lock()  # we don't want the session to expire as long as this
+                         # connection is active
 
         def unlock(signal, profile):
             _session.unlock()
@@ -1392,7 +1487,8 @@
     def actionNewHandler(self, action_data, action_id, security_limit, profile):
         """actionNew handler
 
-        XXX: We need need a dedicated handler has actionNew use a security_limit which must be managed
+        XXX: We need need a dedicated handler has actionNew use a security_limit
+            which must be managed
         @param action_data(dict): see bridge documentation
         @param action_id(unicode): identitifer of the action
         @param security_limit(int): %(doc_security_limit)s
@@ -1423,7 +1519,8 @@
         """Connection is done.
 
         @param profile (unicode): %(doc_profile)s
-        @param jid_s (unicode): the JID that we were assigned by the server, as the resource might differ from the JID we asked for.
+        @param jid_s (unicode): the JID that we were assigned by the server, as the
+            resource might differ from the JID we asked for.
         """
         #  FIXME: _logged should not be called from here, check this code
         #  FIXME: check if needed to connect with external jid
@@ -1442,35 +1539,25 @@
             disconnect_delta = time.time() - self._last_service_prof_disconnect
             if disconnect_delta < 15:
                 log.error(
-                    _(
-                        u"Service profile disconnected twice in a short time, please check connection"
-                    )
-                )
+                    _(u"Service profile disconnected twice in a short time, please "
+                      u"check connection"))
             else:
                 log.info(
-                    _(
-                        u"Service profile has been disconnected, but we need it! Reconnecting it..."
-                    )
-                )
+                    _(u"Service profile has been disconnected, but we need it! "
+                      u"Reconnecting it..."))
                 d = self.sat_host.bridgeCall(
                     "connect", profile, self.sat_host.options["passphrase"], {}
                 )
                 d.addErrback(
-                    lambda failure_: log.error(
-                        _(
-                            u"Can't reconnect service profile, please check connection: {reason}"
-                        ).format(reason=failure_)
-                    )
-                )
+                    lambda failure_: log.error(_(
+                        u"Can't reconnect service profile, please check connection: "
+                        u"{reason}").format(reason=failure_)))
             self._last_service_prof_disconnect = time.time()
             return
 
         if not profile in self.sat_host.prof_connected:
-            log.info(
-                _(
-                    u"'disconnected' signal received for a not connected profile ({profile})"
-                ).format(profile=profile)
-            )
+            log.info(_(u"'disconnected' signal received for a not connected profile "
+                       u"({profile})").format(profile=profile))
             return
         self.sat_host.prof_connected.remove(profile)
         if profile in self.signalDeferred:
@@ -1533,14 +1620,16 @@
         Render method with some hacks:
            - if login is requested, try to login with form data
            - except login, every method is jsonrpc
-           - user doesn't need to be authentified for getSessionMetadata, but must be for all other methods
+           - user doesn't need to be authentified for getSessionMetadata, but must be
+             for all other methods
         """
         filename = self._getFileName(request)
         filepath = os.path.join(self.upload_dir, filename)
         # FIXME: the uploaded file is fully loaded in memory at form parsing time so far
-        #       (see twisted.web.http.Request.requestReceived). A custom requestReceived should
-        #       be written in the futur. In addition, it is not yet possible to get progression informations
-        #       (see http://twistedmatrix.com/trac/ticket/288)
+        #       (see twisted.web.http.Request.requestReceived). A custom requestReceived
+        #       should be written in the futur. In addition, it is not yet possible to
+        #       get progression informations (see
+        #       http://twistedmatrix.com/trac/ticket/288)
 
         with open(filepath, "w") as f:
             f.write(request.args[self.NAME][0])
@@ -1568,7 +1657,8 @@
         return "%s%s" % (
             str(uuid.uuid4()),
             extension,
-        )  # XXX: chromium doesn't seem to play song without the .ogg extension, even with audio/ogg mime-type
+        )  # XXX: chromium doesn't seem to play song without the .ogg extension, even
+           #      with audio/ogg mime-type
 
     def _fileWritten(self, request, filepath):
         """Called once the file is actually written on disk
@@ -1638,6 +1728,21 @@
         self.bridge = Bridge()
         self.bridge.bridgeConnect(callback=self._bridgeCb, errback=self._bridgeEb)
 
+    @property
+    def roots(self):
+        """Return available virtual host roots
+
+        Root resources are only returned once, even if they are present for multiple
+        named vhosts. Order is not relevant, except for default vhost which is always
+        returned first.
+        @return (list[web_resource.Resource]): all vhost root resources
+        """
+        roots = list(set(self.vhost_root.hosts.values()))
+        default = self.vhost_root.default
+        if default is not None and default not in roots:
+            roots.insert(0, default)
+        return roots
+
     def _namespacesGetCb(self, ns_map):
         self.ns_map = ns_map
 
@@ -1649,8 +1754,90 @@
         return os.path.join(u'/', C.TPL_RESOURCE, template_data.site or u'sat',
             C.TEMPLATE_TPL_DIR, template_data.theme, relative_url)
 
-    def backendReady(self, dummy):
-        self.root = root = LiberviaRootResource(self.html_dir)
+    def _moveFirstLevelToDict(self, options, key):
+        """Read a config option and put value at first level into u'' dict
+
+        This is useful to put values for Libervia official site directly in dictionary,
+        and to use site_name as keys when external sites are used.
+        options will be modified in place
+        """
+        try:
+            conf = options[key]
+        except KeyError:
+            return
+        if not isinstance(conf, dict):
+            options[key] = {u'': conf}
+            return
+        default_dict = conf.setdefault(u'', {})
+        to_delete = []
+        for key, value in conf.iteritems():
+            if not isinstance(value, dict):
+                default_dict[key] = value
+                to_delete.append(key)
+        for key in to_delete:
+            del conf[key]
+
+    def backendReady(self, __):
+        self.media_dir = self.bridge.getConfig("", "media_dir")
+        self.local_dir = self.bridge.getConfig("", "local_dir")
+        self.cache_root_dir = os.path.join(self.local_dir, C.CACHE_DIR)
+
+        self._moveFirstLevelToDict(self.options, "url_redirections_dict")
+        self._moveFirstLevelToDict(self.options, "menu_json")
+
+        # we create virtual hosts and import Libervia pages into them
+        self.renderer = template.Renderer(self, self._front_url_filter)
+        self.vhost_root = vhost.NameVirtualHost()
+        default_site_path = os.path.dirname(libervia.__file__)
+        # self.sat_root is official Libervia site
+        self.sat_root = default_root = LiberviaRootResource(
+            host=self, host_name=u'', site_name=u'', site_path=default_site_path,
+            path=self.html_dir)
+        LiberviaPage.importPages(self, self.sat_root)
+        # FIXME: handle _setMenu in a more generic way, taking care of external sites
+        self.sat_root._setMenu(self.options["menu_json"])
+        self.vhost_root.default = default_root
+        existing_vhosts = {u'': default_root}
+
+        for host_name, site_name in self.options["vhosts_dict"].iteritems():
+            try:
+                site_path = self.renderer.sites_paths[site_name]
+            except KeyError:
+                log.warning(_(
+                    u"host {host_name} link to non existing site {site_name}, ignoring "
+                    u"it").format(host_name=host_name, site_name=site_name))
+                continue
+            if site_name in existing_vhosts:
+                # we have an alias host, we re-use existing resource
+                res = existing_vhosts[site_name]
+            else:
+                # for root path we first check if there is a global static dir
+                # if not, we use default template's static dic
+                root_path = os.path.join(site_path, C.TEMPLATE_STATIC_DIR)
+                if not os.path.isdir(root_path):
+                    root_path = os.path.join(
+                        site_path, C.TEMPLATE_TPL_DIR, C.TEMPLATE_THEME_DEFAULT,
+                        C.TEMPLATE_STATIC_DIR)
+                res = LiberviaRootResource(
+                    host=self,
+                    host_name=host_name,
+                    site_name=site_name,
+                    site_path=site_path,
+                    path=root_path)
+            self.vhost_root.addHost(host_name.encode('utf-8'), res)
+            LiberviaPage.importPages(self, res)
+            # FIXME: default pages are accessible if not overriden by external website
+            #        while necessary for login or re-using existing pages
+            #        we may want to disable access to the page by direct URL
+            #        (e.g. /blog disabled except if called by external site)
+            LiberviaPage.importPages(self, res, root_path=default_site_path)
+            res._setMenu(self.options["menu_json"])
+
+        templates_res = web_resource.Resource()
+        self.putChildAll(C.TPL_RESOURCE, templates_res)
+        for site_name, site_path in self.renderer.sites_paths.iteritems():
+            templates_res.putChild(site_name or u'sat', ProtectedFile(site_path))
+
         _register = Register(self)
         _upload_radiocol = UploadManagerRadioCol(self)
         _upload_avatar = UploadManagerAvatar(self)
@@ -1673,7 +1860,8 @@
             self.bridge.register_signal(
                 signal_name, self.signal_handler.getGenericCb(signal_name)
             )
-        # XXX: actionNew is handled separately because the handler must manage security_limit
+        # XXX: actionNew is handled separately because the handler must manage
+        #      security_limit
         self.bridge.register_signal("actionNew", self.signal_handler.actionNewHandler)
         # plugins
         for signal_name in [
@@ -1702,34 +1890,28 @@
             self.bridge.register_signal(
                 signal_name, self.signal_handler.getGenericCb(signal_name), "plugin"
             )
-        self.media_dir = self.bridge.getConfig("", "media_dir")
-        self.local_dir = self.bridge.getConfig("", "local_dir")
-        self.cache_root_dir = os.path.join(self.local_dir, C.CACHE_DIR)
 
         # JSON APIs
-        self.putChild("json_signal_api", self.signal_handler)
-        self.putChild("json_api", MethodHandler(self))
-        self.putChild("register_api", _register)
+        self.putChildSAT("json_signal_api", self.signal_handler)
+        self.putChildSAT("json_api", MethodHandler(self))
+        self.putChildSAT("register_api", _register)
 
         # files upload
-        self.putChild("upload_radiocol", _upload_radiocol)
-        self.putChild("upload_avatar", _upload_avatar)
+        self.putChildSAT("upload_radiocol", _upload_radiocol)
+        self.putChildSAT("upload_avatar", _upload_avatar)
 
         # static pages
-        self.putChild("blog_legacy", MicroBlog(self))
-        self.putChild(C.THEMES_URL, ProtectedFile(self.themes_dir))
+        self.putChildSAT("blog_legacy", MicroBlog(self))
+        self.putChildSAT(C.THEMES_URL, ProtectedFile(self.themes_dir))
 
         # websocket
         if self.options["connection_type"] in ("https", "both"):
             wss = websockets.LiberviaPageWSProtocol.getResource(self, secure=True)
-            self.putChild("wss", wss)
+            self.putChildAll("wss", wss)
         if self.options["connection_type"] in ("http", "both"):
             ws = websockets.LiberviaPageWSProtocol.getResource(self, secure=False)
-            self.putChild("ws", ws)
+            self.putChildAll("ws", ws)
 
-        #  Libervia pages
-        LiberviaPage.importPages(self)
-        LiberviaPage.setMenu(self.options["menu_json"])
         ## following signal is needed for cache handling in Libervia pages
         self.bridge.register_signal(
             "psEventRaw", partial(LiberviaPage.onNodeEvent, self), "plugin"
@@ -1751,33 +1933,33 @@
 
         # media dirs
         # FIXME: get rid of dirname and "/" in C.XXX_DIR
-        self.putChild(os.path.dirname(C.MEDIA_DIR), ProtectedFile(self.media_dir))
+        self.putChildAll(os.path.dirname(C.MEDIA_DIR), ProtectedFile(self.media_dir))
         self.cache_resource = web_resource.NoResource()
-        self.putChild(C.CACHE_DIR, self.cache_resource)
+        self.putChildAll(C.CACHE_DIR, self.cache_resource)
 
         # special
-        self.putChild(
+        self.putChildSAT(
             "radiocol",
             ProtectedFile(_upload_radiocol.getTmpDir(), defaultType="audio/ogg"),
-        )  # FIXME: We cheat for PoC because we know we are on the same host, so we use directly upload dir
+        )  # FIXME: We cheat for PoC because we know we are on the same host, so we use
+           #        directly upload dir
         # pyjamas tests, redirected only for dev versions
         if self.version[-1] == "D":
-            self.putChild("test", web_util.Redirect("/libervia_test.html"))
+            self.putChildSAT("test", web_util.Redirect("/libervia_test.html"))
 
         # redirections
-        root._initRedirections(self.options)
+        for root in self.roots:
+            root._initRedirections(self.options)
+
+        # no need to keep url_redirections_dict, it will not be used anymore
+        del self.options["url_redirections_dict"]
 
         server.Request.defaultContentType = "text/html; charset=utf-8"
         wrapped = web_resource.EncodingResourceWrapper(
-            root, [server.GzipEncoderFactory()]
+            self.vhost_root, [server.GzipEncoderFactory()]
         )
         self.site = server.Site(wrapped)
         self.site.sessionFactory = LiberviaSession
-        self.renderer = template.Renderer(self, self._front_url_filter)
-        templates_res = web_resource.Resource()
-        self.putChild(C.TPL_RESOURCE, templates_res)
-        for site_name, site_path in self.renderer.sites_paths.iteritems():
-            templates_res.putChild(site_name or u'sat', ProtectedFile(site_path))
 
     def initEb(self, failure):
         log.error(_(u"Init error: {msg}").format(msg=failure))
@@ -1806,7 +1988,7 @@
 
     @property
     def full_version(self):
-        """Return the full version of Libervia (with extra data when in development mode)"""
+        """Return the full version of Libervia (with extra data when in dev mode)"""
         version = self.version
         if version[-1] == "D":
             # we are in debug version, we add extra data
@@ -1873,7 +2055,8 @@
         cache_dir = os.path.join(
             self.cache_root_dir, u"profiles", regex.pathEscape(profile)
         )
-        # FIXME: would be better to have a global /cache URL which redirect to profile's cache directory, without uuid
+        # FIXME: would be better to have a global /cache URL which redirect to
+        #        profile's cache directory, without uuid
         self.cache_resource.putChild(sat_session.uuid, ProtectedFile(cache_dir))
         log.debug(
             _(u"profile cache resource added from {uuid} to {path}").format(
@@ -1916,10 +2099,12 @@
             can be profile@[libervia_domain.ext]
             can be a jid (a new profile will be created with this jid if needed)
         @param password(unicode): user password
-        @return (unicode, None): C.SESSION_ACTIVE: if session was aleady active else self._logged value
+        @return (unicode, None): C.SESSION_ACTIVE: if session was aleady active else
+            self._logged value
         @raise exceptions.DataError: invalid login
         @raise exceptions.ProfileUnknownError: this login doesn't exist
-        @raise exceptions.PermissionError: a login is not accepted (e.g. empty password not allowed)
+        @raise exceptions.PermissionError: a login is not accepted (e.g. empty password
+            not allowed)
         @raise exceptions.NotReady: a profile connection is already waiting
         @raise exceptions.TimeoutError: didn't received and answer from Bridge
         @raise exceptions.InternalError: unknown error
@@ -1962,8 +2147,8 @@
             ):  # try to create a new sat profile using the XMPP credentials
                 if not self.options["allow_registration"]:
                     log.warning(
-                        u"Trying to register JID account while registration is not allowed"
-                    )
+                        u"Trying to register JID account while registration is not "
+                        u"allowed")
                     raise failure.Failure(
                         exceptions.DataError(
                             u"JID login while registration is not allowed"
@@ -1992,11 +2177,10 @@
             # yes, there is
             if sat_session.profile != profile:
                 # it's a different profile, we need to disconnect it
-                log.warning(
-                    _(
-                        u"{new_profile} requested login, but {old_profile} was already connected, disconnecting {old_profile}"
-                    ).format(old_profile=sat_session.profile, new_profile=profile)
-                )
+                log.warning(_(
+                    u"{new_profile} requested login, but {old_profile} was already "
+                    u"connected, disconnecting {old_profile}").format(
+                        old_profile=sat_session.profile, new_profile=profile))
                 self.purgeSession(request)
 
         if self.waiting_profiles.getRequest(profile):
@@ -2010,32 +2194,21 @@
             fault = failure_.faultString
             self.waiting_profiles.purgeRequest(profile)
             if fault in ("PasswordError", "ProfileUnknownError"):
-                log.info(
-                    u"Profile {profile} doesn't exist or the submitted password is wrong".format(
-                        profile=profile
-                    )
-                )
+                log.info(u"Profile {profile} doesn't exist or the submitted password is "
+                         u"wrong".format( profile=profile))
                 raise failure.Failure(ValueError(C.PROFILE_AUTH_ERROR))
             elif fault == "SASLAuthError":
-                log.info(
-                    u"The XMPP password of profile {profile} is wrong".format(
-                        profile=profile
-                    )
-                )
+                log.info(u"The XMPP password of profile {profile} is wrong"
+                    .format(profile=profile))
                 raise failure.Failure(ValueError(C.XMPP_AUTH_ERROR))
             elif fault == "NoReply":
-                log.info(
-                    _(
-                        "Did not receive a reply (the timeout expired or the connection is broken)"
-                    )
-                )
+                log.info(_(u"Did not receive a reply (the timeout expired or the "
+                           u"connection is broken)"))
                 raise exceptions.TimeOutError
             else:
-                log.error(
-                    u'Unmanaged fault string "{fault}" in errback for the connection of profile {profile}'.format(
-                        fault=fault, profile=profile
-                    )
-                )
+                log.error(u'Unmanaged fault string "{fault}" in errback for the '
+                          u'connection of profile {profile}'.format(
+                              fault=fault, profile=profile))
                 raise failure.Failure(exceptions.InternalError(fault))
 
         if connected:
@@ -2047,11 +2220,10 @@
                 if sat_session.profile != profile:
                     # existing session should have been ended above
                     # so this line should never be reached
-                    log.error(
-                        _(
-                            u"session profile [{session_profile}] differs from login profile [{profile}], this should not happen!"
-                        ).format(session_profile=sat_session.profile, profile=profile)
-                    )
+                    log.error(_(
+                        u"session profile [{session_profile}] differs from login "
+                        u"profile [{profile}], this should not happen!")
+                            .format(session_profile=sat_session.profile, profile=profile))
                     raise exceptions.InternalError("profile mismatch")
                 defer.returnValue(C.SESSION_ACTIVE)
             log.info(
@@ -2133,17 +2305,15 @@
             log.error(_(u"Connection failed: %s") % e)
             self.stop()
 
-        def initOk(dummy):
+        def initOk(__):
             try:
                 connected = self.bridge.isConnected(C.SERVICE_PROFILE)
             except Exception as e:
                 # we don't want the traceback
                 msg = [l for l in unicode(e).split("\n") if l][-1]
                 log.error(
-                    u"Can't check service profile ({profile}), are you sure it exists ?\n{error}".format(
-                        profile=C.SERVICE_PROFILE, error=msg
-                    )
-                )
+                    u"Can't check service profile ({profile}), are you sure it exists ?"
+                    u"\n{error}".format(profile=C.SERVICE_PROFILE, error=msg))
                 self.stop()
                 return
             if not connected:
@@ -2161,13 +2331,19 @@
 
     ## URLs ##
 
-    def putChild(self, path, resource):
-        """Add a child to the root resource"""
+    def putChildSAT(self, path, resource):
+        """Add a child to the sat resource"""
+        self.sat_root.putChild(path, resource)
+
+    def putChildAll(self, path, resource):
+        """Add a child to all vhost root resources"""
+        # we wrap before calling putChild, to avoid having useless multiple instances
+        # of the resource
         # FIXME: check that no information is leaked (c.f. https://twistedmatrix.com/documents/current/web/howto/using-twistedweb.html#request-encoders)
-        self.root.putChild(
-            path,
-            web_resource.EncodingResourceWrapper(resource, [server.GzipEncoderFactory()]),
-        )
+        wrapped_res = web_resource.EncodingResourceWrapper(
+            resource, [server.GzipEncoderFactory()])
+        for root in self.roots:
+            root.putChild(path, wrapped_res)
 
     def getExtBaseURLData(self, request):
         """Retrieve external base URL Data
@@ -2177,7 +2353,8 @@
             - base_url_ext option from configuration
             - proxy x-forwarder-host headers
             - URL of the request
-        @return (urlparse.SplitResult): SplitResult instance with only scheme and netloc filled
+        @return (urlparse.SplitResult): SplitResult instance with only scheme and
+            netloc filled
         """
         ext_data = self.base_url_ext_data
         url_path = request.URLPath()
@@ -2245,13 +2422,14 @@
             )
         )
 
-    def checkRedirection(self, url):
+    def checkRedirection(self, vhost_root, url):
         """check is a part of the URL prefix is redirected then replace it
 
+        @param vhost_root(web_resource.Resource): root of this virtual host
         @param url(unicode): url to check
         @return (unicode): possibly redirected URL which should link to the same location
         """
-        inv_redirections = self.root.inv_redirections
+        inv_redirections = vhost_root.inv_redirections
         url_parts = url.strip(u"/").split(u"/")
         for idx in xrange(len(url), 0, -1):
             test_url = u"/" + u"/".join(url_parts[:idx])
@@ -2434,26 +2612,23 @@
                 self.quit(2)
             except OpenSSL.crypto.Error:
                 log.error(
-                    u"Error while parsing file {path} for option {option}, are you sure it is a valid .pem file?".format(
-                        path=path, option=option
-                    )
-                )
+                    u"Error while parsing file {path} for option {option}, are you sure "
+                    u"it is a valid .pem file?".format( path=path, option=option))
                 if (
                     option == "tls_private_key"
                     and self.options["tls_certificate"] == path
                 ):
                     log.error(
-                        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(
-                            path=path
-                        )
-                    )
+                        u"You are using the same file for private key and public "
+                        u"certificate, make sure that both a in {path} or use "
+                        u"--tls_private_key option".format(path=path))
                 self.quit(2)
 
         return ssl.CertificateOptions(**cert_options)
 
     ## service management ##
 
-    def _startService(self, dummy=None):
+    def _startService(self, __=None):
         """Actually start the HTTP(S) server(s) after the profile for Libervia is connected.
 
         @raise ImportError: OpenSSL is not available
--- a/twisted/plugins/libervia_server.py	Sun Sep 09 21:12:22 2018 +0200
+++ b/twisted/plugins/libervia_server.py	Fri Sep 14 21:41:28 2018 +0200
@@ -158,7 +158,7 @@
 # Options which are in sat.conf only
 OPT_PARAMETERS_CFG = [
     ["empty_password_allowed_warning_dangerous_list", None, "", None],
-    ["url_redirections_profile", None, "", None],
+    ["vhosts_dict", None, {}, None],
     ["url_redirections_dict", None, {}, None],
     ["menu_json", None, C.DEFAULT_MENU, None],
     ["tickets_trackers_json", None, None, None],