diff libervia/server/server.py @ 1124:28e3eb3bb217

files reorganisation and installation rework: - files have been reorganised to follow other SàT projects and usual Python organisation (no more "/src" directory) - VERSION file is now used, as for other SàT projects - replace the overcomplicated setup.py be a more sane one. Pyjamas part is not compiled anymore by setup.py, it must be done separatly - removed check for data_dir if it's empty - installation tested working in virtual env - libervia launching script is now in bin/libervia
author Goffi <goffi@goffi.org>
date Sat, 25 Aug 2018 17:59:48 +0200
parents src/server/server.py@cdd389ef97bc
children 7cd89277a129
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/server/server.py	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,2525 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from twisted.application import service
+from twisted.internet import reactor, defer
+from twisted.web import server
+from twisted.web import static
+from twisted.web import resource as web_resource
+from twisted.web import util as web_util
+from twisted.web import http
+from twisted.python.components import registerAdapter
+from twisted.python import failure
+from twisted.words.protocols.jabber import jid
+
+from txjsonrpc.web import jsonrpc
+from txjsonrpc import jsonrpclib
+
+from sat.core.log import getLogger
+
+log = getLogger(__name__)
+from sat_frontends.bridge.dbus_bridge import (
+    Bridge,
+    BridgeExceptionNoService,
+    const_TIMEOUT as BRIDGE_TIMEOUT,
+)
+from sat.core.i18n import _, D_
+from sat.core import exceptions
+from sat.tools import utils
+from sat.tools.common import regex
+from sat.tools.common import template
+
+import re
+import glob
+import os.path
+import sys
+import tempfile
+import shutil
+import uuid
+import urlparse
+import urllib
+import time
+from httplib import HTTPS_PORT
+import libervia
+from libervia.server import websockets
+from libervia.server.pages import LiberviaPage
+from libervia.server.utils import quote, ProgressHandler
+from functools import partial
+
+try:
+    import OpenSSL
+    from twisted.internet import ssl
+except ImportError:
+    ssl = None
+
+from libervia.server.constants import Const as C
+from libervia.server.blog import MicroBlog
+from libervia.server import session_iface
+
+
+# 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
+
+
+class LiberviaSession(server.Session):
+    sessionTimeout = C.SESSION_TIMEOUT
+
+    def __init__(self, *args, **kwargs):
+        self.__lock = False
+        server.Session.__init__(self, *args, **kwargs)
+
+    def lock(self):
+        """Prevent session from expiring"""
+        self.__lock = True
+        self._expireCall.reset(sys.maxint)
+
+    def unlock(self):
+        """Allow session to expire again, and touch it"""
+        self.__lock = False
+        self.touch()
+
+    def touch(self):
+        if not self.__lock:
+            server.Session.touch(self)
+
+
+class ProtectedFile(static.File):
+    """A static.File class which doens't show directory listing"""
+
+    def directoryListing(self):
+        return web_resource.NoResource()
+
+
+class LiberviaRootResource(ProtectedFile):
+    """Specialized resource for Libervia root
+
+    handle redirections declared in sat.conf
+    """
+
+    def _initRedirections(self, options):
+        ## 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():
+            # 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)
+                # 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'
+                    )
+                if "url" in new_data:
+                    new = new_data["url"]
+                elif "page" in new_data:
+                    new = new_data
+                    new["type"] = "page"
+                    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
+                                )
+                            )
+                        )
+                        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
+                                )
+                            )
+                        )
+                        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
+                    args_hash = tuple(new["path_args"])
+                    LiberviaPage.pages_redirects.setdefault(new_data["page"], {})[
+                        args_hash
+                    ] = old
+
+                    # we need lists in query_args because it will be used
+                    # as it in request.path_args
+                    for k, v in new["query_args"].iteritems():
+                        if isinstance(v, basestring):
+                            new["query_args"][k] = [v]
+                elif "path" in new_data:
+                    new = "file:{}".format(urllib.quote(new_data["path"]))
+            elif isinstance(new_data, basestring):
+                new = new_data
+                new_data = {}
+            else:
+                log.error(
+                    _(u"ignoring invalid redirection value: {new_data}").format(
+                        new_data=new_data
+                    )
+                )
+                continue
+
+            # some normalization
+            if not old.strip():
+                # root URL special case
+                old = ""
+            elif not old.startswith("/"):
+                log.error(
+                    _(
+                        u"redirected url must start with '/', got {value}. Ignoring"
+                    ).format(value=old)
+                )
+                continue
+            else:
+                old = self._normalizeURL(old)
+
+            if isinstance(new, dict):
+                # dict are handled differently, they contain data
+                # which ared use dynamically when the request is done
+                self.redirections[old] = new
+                if not old:
+                    if new[u"type"] == u"page":
+                        log.info(
+                            _(u"Root URL redirected to page {name}").format(
+                                name=new[u"page"]
+                            )
+                        )
+                else:
+                    if new[u"type"] == u"page":
+                        page = LiberviaPage.getPageByName(new[u"page"])
+                        url = page.getURL(*new.get(u"path_args", []))
+                        self.inv_redirections[url] = old
+                continue
+
+            # at this point we have a redirection URL in new, we can parse it
+            new_url = urlparse.urlsplit(new.encode("utf-8"))
+
+            # we handle the known URL schemes
+            if new_url.scheme == "xmpp":
+                location = LiberviaPage.getPagePathFromURI(new)
+                if location is None:
+                    log.warning(
+                        _(
+                            u"ignoring redirection, no page found to handle this URI: {uri}"
+                        ).format(uri=new)
+                    )
+                    continue
+                request_data = self._getRequestData(location)
+                if old:
+                    self.inv_redirections[location] = old
+
+            elif new_url.scheme in ("", "http", "https"):
+                # 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
+                        )
+                    )
+                location = urlparse.urlunsplit(
+                    ("", "", new_url.path, new_url.query, new_url.fragment)
+                ).decode("utf-8")
+                request_data = self._getRequestData(location)
+                if old:
+                    self.inv_redirections[location] = old
+
+            elif new_url.scheme in ("file"):
+                # 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
+                        )
+                    )
+                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"
+                    )
+                # for file redirection, we directly put child here
+                segments, dummy, last_segment = old.rpartition("/")
+                url_segments = segments.split("/") if segments else []
+                current = self
+                for segment in url_segments:
+                    resource = web_resource.NoResource()
+                    current.putChild(segment, resource)
+                    current = resource
+                resource_class = (
+                    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")
+                    )
+                )
+                continue  # we don't want to use redirection system, so we continue here
+
+            else:
+                raise NotImplementedError(
+                    u"{scheme}: scheme is not managed for url_redirections_dict".format(
+                        scheme=new_url.scheme
+                    )
+                )
+
+            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"]
+
+        # the default root URL, if not redirected
+        if not "" in self.redirections:
+            self.redirections[""] = self._getRequestData(C.LIBERVIA_MAIN_PAGE)
+
+    def _normalizeURL(self, url, lower=True):
+        """Return URL normalized for self.redirections dict
+
+        @param url(unicode): URL to normalize
+        @param lower(bool): lower case of url if True
+        @return (str): normalized URL
+        """
+        if lower:
+            url = url.lower()
+        return "/".join((p for p in url.encode("utf-8").split("/") if p))
+
+    def _getRequestData(self, uri):
+        """Return data needed to redirect request
+
+        @param url(unicode): destination url
+        @return (tuple(list[str], str, str, dict): tuple with
+            splitted path as in Request.postpath
+            uri as in Request.uri
+            path as in Request.path
+            args as in Request.args
+        """
+        uri = uri.encode("utf-8")
+        # XXX: we reuse code from twisted.web.http.py here
+        #      as we need to have the same behaviour
+        x = uri.split(b"?", 1)
+
+        if len(x) == 1:
+            path = uri
+            args = {}
+        else:
+            path, argstring = x
+            args = http.parse_qs(argstring, 1)
+
+        # XXX: splitted path case must not be changed, as it may be significant
+        #      (e.g. for blog items)
+        return (
+            self._normalizeURL(path.decode("utf-8"), lower=False).split("/"),
+            uri,
+            path,
+            args,
+        )
+
+    def _redirect(self, request, request_data):
+        """Redirect an URL by rewritting request
+
+        this is *NOT* a HTTP redirection, but equivalent to URL rewritting
+        @param request(web.http.request): original request
+        @param request_data(tuple): data returned by self._getRequestData
+        @return (web_resource.Resource): resource to use
+        """
+        # recursion check
+        try:
+            request._redirected
+        except AttributeError:
+            pass
+        else:
+            try:
+                dummy, uri, dummy, dummy = 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"))
+            )
+            return web_resource.NoResource()
+
+        request._redirected = True  # here to avoid recursive redirections
+
+        if isinstance(request_data, dict):
+            if request_data["type"] == "page":
+                try:
+                    page = LiberviaPage.getPageByName(request_data["page"])
+                except KeyError:
+                    log.error(
+                        _(
+                            u'Can\'t find page named "{name}" requested in redirection'
+                        ).format(name=request_data["page"])
+                    )
+                    return web_resource.NoResource()
+                request.postpath = request_data["path_args"][:] + request.postpath
+
+                try:
+                    request.args.update(request_data["query_args"])
+                except (TypeError, ValueError):
+                    log.error(
+                        _(u"Invalid args in redirection: {query_args}").format(
+                            query_args=request_data["query_args"]
+                        )
+                    )
+                    return web_resource.NoResource()
+                return page
+            else:
+                raise exceptions.InternalError(u"unknown request_data type")
+        else:
+            path_list, uri, path, args = request_data
+            log.debug(
+                u"Redirecting URL {old} to {new}".format(
+                    old=request.uri.decode("utf-8"), new=uri.decode("utf-8")
+                )
+            )
+            # we change the request to reflect the new url
+            request.postpath = path_list[1:] + request.postpath
+            request.args = args
+
+        # we start again to look for a child with the new url
+        return self.getChildWithDefault(path_list[0], request)
+
+    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
+        if name == "" and not request.postpath:
+            return self._redirect(request, self.redirections[""])
+        return super(LiberviaRootResource, self).getChildWithDefault(name, request)
+
+    def getChild(self, name, request):
+        resource = super(LiberviaRootResource, self).getChild(name, request)
+
+        if isinstance(resource, web_resource.NoResource):
+            # if nothing was found, we try our luck with redirections
+            # XXX: we want redirections to happen only if everything else failed
+            path_elt = request.prepath + request.postpath
+            for idx in xrange(len(path_elt), 0, -1):
+                test_url = "/".join(path_elt[:idx]).lower()
+                if test_url in self.redirections:
+                    request_data = self.redirections[test_url]
+                    request.postpath = path_elt[idx:]
+                    return self._redirect(request, request_data)
+
+        return resource
+
+    def createSimilarFile(self, path):
+        # XXX: this method need to be overriden to avoid recreating a LiberviaRootResource
+
+        f = LiberviaRootResource.__base__(
+            path, self.defaultType, self.ignoredExts, self.registry
+        )
+        # refactoring by steps, here - constructor should almost certainly take these
+        f.processors = self.processors
+        f.indexNames = self.indexNames[:]
+        f.childNotFound = self.childNotFound
+        return f
+
+
+class JSONRPCMethodManager(jsonrpc.JSONRPC):
+    def __init__(self, sat_host):
+        jsonrpc.JSONRPC.__init__(self)
+        self.sat_host = sat_host
+
+    def asyncBridgeCall(self, method_name, *args, **kwargs):
+        return self.sat_host.bridgeCall(method_name, *args, **kwargs)
+
+
+class MethodHandler(JSONRPCMethodManager):
+    def __init__(self, sat_host):
+        JSONRPCMethodManager.__init__(self, sat_host)
+
+    def render(self, request):
+        self.session = request.getSession()
+        profile = session_iface.ISATSession(self.session).profile
+        if not profile:
+            # user is not identified, we return a jsonrpc fault
+            parsed = jsonrpclib.loads(request.content.read())
+            fault = jsonrpclib.Fault(
+                C.ERRNUM_LIBERVIA, C.NOT_ALLOWED
+            )  # FIXME: define some standard error codes for libervia
+            return jsonrpc.JSONRPC._cbRender(
+                self, fault, request, parsed.get("id"), parsed.get("jsonrpc")
+            )  # pylint: disable=E1103
+        return jsonrpc.JSONRPC.render(self, request)
+
+    @defer.inlineCallbacks
+    def jsonrpc_getVersion(self):
+        """Return SàT version"""
+        try:
+            defer.returnValue(self._version_cache)
+        except AttributeError:
+            self._version_cache = yield self.sat_host.bridgeCall("getVersion")
+            defer.returnValue(self._version_cache)
+
+    def jsonrpc_getLiberviaVersion(self):
+        """Return Libervia version"""
+        return self.sat_host.full_version
+
+    def jsonrpc_disconnect(self):
+        """Disconnect the profile"""
+        sat_session = session_iface.ISATSession(self.session)
+        profile = sat_session.profile
+        self.sat_host.bridgeCall("disconnect", profile)
+
+    def jsonrpc_getContacts(self):
+        """Return all passed args."""
+        profile = session_iface.ISATSession(self.session).profile
+        return self.sat_host.bridgeCall("getContacts", profile)
+
+    @defer.inlineCallbacks
+    def jsonrpc_addContact(self, entity, name, groups):
+        """Subscribe to contact presence, and add it to the given groups"""
+        profile = session_iface.ISATSession(self.session).profile
+        yield self.sat_host.bridgeCall("addContact", entity, profile)
+        yield self.sat_host.bridgeCall("updateContact", entity, name, groups, profile)
+
+    def jsonrpc_delContact(self, entity):
+        """Remove contact from contacts list"""
+        profile = session_iface.ISATSession(self.session).profile
+        return self.sat_host.bridgeCall("delContact", entity, profile)
+
+    def jsonrpc_updateContact(self, entity, name, groups):
+        """Update contact's roster item"""
+        profile = session_iface.ISATSession(self.session).profile
+        return self.sat_host.bridgeCall("updateContact", entity, name, groups, profile)
+
+    def jsonrpc_subscription(self, sub_type, entity):
+        """Confirm (or infirm) subscription,
+        and setup user roster in case of subscription"""
+        profile = session_iface.ISATSession(self.session).profile
+        return self.sat_host.bridgeCall("subscription", sub_type, entity, profile)
+
+    def jsonrpc_getWaitingSub(self):
+        """Return list of room already joined by user"""
+        profile = session_iface.ISATSession(self.session).profile
+        return self.sat_host.bridgeCall("getWaitingSub", profile)
+
+    def jsonrpc_setStatus(self, presence, status):
+        """Change the presence and/or status
+        @param presence: value from ("", "chat", "away", "dnd", "xa")
+        @param status: any string to describe your status
+        """
+        profile = session_iface.ISATSession(self.session).profile
+        return self.sat_host.bridgeCall(
+            "setPresence", "", presence, {"": status}, profile
+        )
+
+    def jsonrpc_messageSend(self, to_jid, msg, subject, type_, extra={}):
+        """send message"""
+        profile = session_iface.ISATSession(self.session).profile
+        return self.asyncBridgeCall(
+            "messageSend", to_jid, msg, subject, type_, extra, profile
+        )
+
+    ## PubSub ##
+
+    def jsonrpc_psNodeDelete(self, service, node):
+        """Delete a whole node
+
+        @param service (unicode): service jid
+        @param node (unicode): node to delete
+        """
+        profile = session_iface.ISATSession(self.session).profile
+        return self.asyncBridgeCall("psNodeDelete", service, node, profile)
+
+    # def jsonrpc_psRetractItem(self, service, node, item, notify):
+    #     """Delete a whole node
+
+    #     @param service (unicode): service jid
+    #     @param node (unicode): node to delete
+    #     @param items (iterable): id of item to retract
+    #     @param notify (bool): True if notification is required
+    #     """
+    #     profile = session_iface.ISATSession(self.session).profile
+    #     return self.asyncBridgeCall("psRetractItem", service, node, item, notify, profile)
+
+    # def jsonrpc_psRetractItems(self, service, node, items, notify):
+    #     """Delete a whole node
+
+    #     @param service (unicode): service jid
+    #     @param node (unicode): node to delete
+    #     @param items (iterable): ids of items to retract
+    #     @param notify (bool): True if notification is required
+    #     """
+    #     profile = session_iface.ISATSession(self.session).profile
+    #     return self.asyncBridgeCall("psRetractItems", service, node, items, notify, profile)
+
+    ## microblogging ##
+
+    def jsonrpc_mbSend(self, service, node, mb_data):
+        """Send microblog data
+
+        @param service (unicode): service jid or empty string to use profile's microblog
+        @param node (unicode): publishing node, or empty string to use microblog node
+        @param mb_data(dict): microblog data
+        @return: a deferred
+        """
+        profile = session_iface.ISATSession(self.session).profile
+        return self.asyncBridgeCall("mbSend", service, node, mb_data, profile)
+
+    def jsonrpc_mbRetract(self, service, node, items):
+        """Delete a whole node
+
+        @param service (unicode): service jid, empty string for PEP
+        @param node (unicode): node to delete, empty string for default node
+        @param items (iterable): ids of items to retract
+        """
+        profile = session_iface.ISATSession(self.session).profile
+        return self.asyncBridgeCall("mbRetract", service, node, items, profile)
+
+    def jsonrpc_mbGet(self, service_jid, node, max_items, item_ids, extra):
+        """Get last microblogs from publisher_jid
+
+        @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 item_ids (list[unicode]): list of item IDs
+        @param rsm (dict): TODO
+        @return: a deferred couple with the list of items and metadatas.
+        """
+        profile = session_iface.ISATSession(self.session).profile
+        return self.asyncBridgeCall(
+            "mbGet", service_jid, node, max_items, item_ids, extra, profile
+        )
+
+    def jsonrpc_mbGetFromMany(self, publishers_type, publishers, max_items, extra):
+        """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 extra (dict): TODO
+        @return (str): RT Deferred session id
+        """
+        profile = session_iface.ISATSession(self.session).profile
+        return self.sat_host.bridgeCall(
+            "mbGetFromMany", publishers_type, publishers, max_items, extra, profile
+        )
+
+    def jsonrpc_mbGetFromManyRTResult(self, rt_session):
+        """Get results from RealTime mbGetFromMany session
+
+        @param rt_session (str): RT Deferred session id
+        """
+        profile = session_iface.ISATSession(self.session).profile
+        return self.asyncBridgeCall("mbGetFromManyRTResult", rt_session, profile)
+
+    def jsonrpc_mbGetFromManyWithComments(
+        self,
+        publishers_type,
+        publishers,
+        max_items,
+        max_comments,
+        rsm_dict,
+        rsm_comments_dict,
+    ):
+        """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 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
+        @param rsm_comments_dict (dict): RSM data for comments only
+        @param profile_key: profile key
+        @return (str): RT Deferred session id
+        """
+        profile = session_iface.ISATSession(self.session).profile
+        return self.sat_host.bridgeCall(
+            "mbGetFromManyWithComments",
+            publishers_type,
+            publishers,
+            max_items,
+            max_comments,
+            rsm_dict,
+            rsm_comments_dict,
+            profile,
+        )
+
+    def jsonrpc_mbGetFromManyWithCommentsRTResult(self, rt_session):
+        """Get results from RealTime mbGetFromManyWithComments session
+
+        @param rt_session (str): RT Deferred session id
+        """
+        profile = session_iface.ISATSession(self.session).profile
+        return self.asyncBridgeCall(
+            "mbGetFromManyWithCommentsRTResult", rt_session, profile
+        )
+
+    # def jsonrpc_sendMblog(self, type_, dest, text, extra={}):
+    #     """ Send microblog message
+    #     @param type_ (unicode): one of "PUBLIC", "GROUP"
+    #     @param dest (tuple(unicode)): recipient groups (ignored for "PUBLIC")
+    #     @param text (unicode): microblog's text
+    #     """
+    #     profile = session_iface.ISATSession(self.session).profile
+    #     extra['allow_comments'] = 'True'
+
+    #     if not type_:  # auto-detect
+    #         type_ = "PUBLIC" if dest == [] else "GROUP"
+
+    #     if type_ in ("PUBLIC", "GROUP") and text:
+    #         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)
+    #         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)
+    #     else:
+    #         raise Exception("Invalid data")
+
+    # def jsonrpc_deleteMblog(self, pub_data, comments):
+    #     """Delete a microblog node
+    #     @param pub_data: a tuple (service, comment node identifier, item identifier)
+    #     @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)
+
+    # def jsonrpc_updateMblog(self, pub_data, comments, message, extra={}):
+    #     """Modify a microblog node
+    #     @param pub_data: a tuple (service, comment node identifier, item identifier)
+    #     @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)
+    #         - 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)
+
+    # def jsonrpc_sendMblogComment(self, node, text, extra={}):
+    #     """ Send microblog message
+    #     @param node: url of the comments node
+    #     @param text: comment
+    #     """
+    #     profile = session_iface.ISATSession(self.session).profile
+    #     if node and text:
+    #         return self.sat_host.bridge.sendGroupBlogComment(node, text, extra, profile)
+    #     else:
+    #         raise Exception("Invalid data")
+
+    # def jsonrpc_getMblogs(self, publisher_jid, item_ids, max_items=C.RSM_MAX_ITEMS):
+    #     """Get specified microblogs posted by a contact
+    #     @param publisher_jid: jid of the publisher
+    #     @param item_ids: list of microblogs items IDs
+    #     @return list of microblog data (dict)"""
+    #     profile = session_iface.ISATSession(self.session).profile
+    #     d = self.asyncBridgeCall("getGroupBlogs", publisher_jid, item_ids, {'max_': unicode(max_items)}, False, profile)
+    #     return d
+
+    # def jsonrpc_getMblogsWithComments(self, publisher_jid, item_ids, max_comments=C.RSM_MAX_COMMENTS):
+    #     """Get specified microblogs posted by a contact and their comments
+    #     @param publisher_jid: jid of the publisher
+    #     @param item_ids: list of microblogs items IDs
+    #     @return list of couple (microblog data, list of microblog data)"""
+    #     profile = session_iface.ISATSession(self.session).profile
+    #     d = self.asyncBridgeCall("getGroupBlogsWithComments", publisher_jid, item_ids, {}, max_comments, profile)
+    #     return d
+
+    # def jsonrpc_getMassiveMblogs(self, publishers_type, publishers, rsm=None):
+    #     """Get lasts microblogs posted by several contacts 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 rsm (dict): TODO
+    #     @return: dict{unicode: list[dict])
+    #         key: publisher's jid
+    #         value: list of microblog data (dict)
+    #     """
+    #     profile = session_iface.ISATSession(self.session).profile
+    #     if rsm is None:
+    #         rsm = {'max_': unicode(C.RSM_MAX_ITEMS)}
+    #     d = self.asyncBridgeCall("getMassiveGroupBlogs", publishers_type, publishers, rsm, profile)
+    #     self.sat_host.bridge.massiveSubscribeGroupBlogs(publishers_type, publishers, profile)
+    #     return d
+
+    # def jsonrpc_getMblogComments(self, service, node, rsm=None):
+    #     """Get all comments of given node
+    #     @param service: jid of the service hosting the node
+    #     @param node: comments node
+    #     """
+    #     profile = session_iface.ISATSession(self.session).profile
+    #     if rsm is None:
+    #         rsm = {'max_': unicode(C.RSM_MAX_COMMENTS)}
+    #     d = self.asyncBridgeCall("getGroupBlogComments", service, node, rsm, profile)
+    #     return d
+
+    def jsonrpc_getPresenceStatuses(self):
+        """Get Presence information for connected contacts"""
+        profile = session_iface.ISATSession(self.session).profile
+        return self.sat_host.bridgeCall("getPresenceStatuses", profile)
+
+    def jsonrpc_historyGet(self, from_jid, to_jid, size, between, search=""):
+        """Return history for the from_jid/to_jid couple"""
+        sat_session = session_iface.ISATSession(self.session)
+        profile = sat_session.profile
+        sat_jid = sat_session.jid
+        if not sat_jid:
+            raise exceptions.InternalError("session jid should be set")
+        if (
+            jid.JID(from_jid).userhost() != sat_jid.userhost()
+            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
+                )
+            )
+            return {}
+        d = self.asyncBridgeCall(
+            "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
+                uuid, timestamp, from_jid, to_jid, message, subject, mess_type, extra = (
+                    line
+                )
+                result.append(
+                    (
+                        unicode(uuid),
+                        float(timestamp),
+                        unicode(from_jid),
+                        unicode(to_jid),
+                        dict(message),
+                        dict(subject),
+                        unicode(mess_type),
+                        dict(extra),
+                    )
+                )
+            return result
+
+        d.addCallback(show)
+        return d
+
+    def jsonrpc_mucJoin(self, room_jid, nick):
+        """Join a Multi-User Chat room
+
+        @param room_jid (unicode): room JID or empty string to generate a unique name
+        @param nick (unicode): user nick
+        """
+        profile = session_iface.ISATSession(self.session).profile
+        d = self.asyncBridgeCall("joinMUC", room_jid, nick, {}, profile)
+        return d
+
+    def jsonrpc_inviteMUC(self, contact_jid, room_jid):
+        """Invite a user to a Multi-User Chat room
+
+        @param contact_jid (unicode): contact to invite
+        @param room_jid (unicode): room JID or empty string to generate a unique name
+        """
+        profile = session_iface.ISATSession(self.session).profile
+        room_id = room_jid.split("@")[0]
+        service = room_jid.split("@")[1]
+        return self.sat_host.bridgeCall(
+            "inviteMUC", contact_jid, service, room_id, {}, profile
+        )
+
+    def jsonrpc_mucLeave(self, room_jid):
+        """Quit a Multi-User Chat room"""
+        profile = session_iface.ISATSession(self.session).profile
+        try:
+            room_jid = jid.JID(room_jid)
+        except:
+            log.warning("Invalid room jid")
+            return
+        return self.sat_host.bridgeCall("mucLeave", room_jid.userhost(), profile)
+
+    def jsonrpc_mucGetRoomsJoined(self):
+        """Return list of room already joined by user"""
+        profile = session_iface.ISATSession(self.session).profile
+        return self.sat_host.bridgeCall("mucGetRoomsJoined", profile)
+
+    def jsonrpc_mucGetDefaultService(self):
+        """@return: the default MUC"""
+        d = self.asyncBridgeCall("mucGetDefaultService")
+        return d
+
+    def jsonrpc_launchTarotGame(self, other_players, room_jid=""):
+        """Create a room, invite the other players and start a Tarot game.
+
+        @param other_players (list[unicode]): JIDs of the players to play with
+        @param room_jid (unicode): room JID or empty string to generate a unique name
+        """
+        profile = session_iface.ISATSession(self.session).profile
+        return self.sat_host.bridgeCall(
+            "tarotGameLaunch", other_players, room_jid, profile
+        )
+
+    def jsonrpc_getTarotCardsPaths(self):
+        """Give the path of all the tarot cards"""
+        _join = os.path.join
+        _media_dir = _join(self.sat_host.media_dir, "")
+        return map(
+            lambda x: _join(C.MEDIA_DIR, x[len(_media_dir) :]),
+            glob.glob(_join(_media_dir, C.CARDS_DIR, "*_*.png")),
+        )
+
+    def jsonrpc_tarotGameReady(self, player, referee):
+        """Tell to the server that we are ready to start the game"""
+        profile = session_iface.ISATSession(self.session).profile
+        return self.sat_host.bridgeCall("tarotGameReady", player, referee, profile)
+
+    def jsonrpc_tarotGamePlayCards(self, player_nick, referee, cards):
+        """Tell to the server the cards we want to put on the table"""
+        profile = session_iface.ISATSession(self.session).profile
+        return self.sat_host.bridgeCall(
+            "tarotGamePlayCards", player_nick, referee, cards, profile
+        )
+
+    def jsonrpc_launchRadioCollective(self, invited, room_jid=""):
+        """Create a room, invite people, and start a radio collective.
+
+        @param invited (list[unicode]): JIDs of the contacts to play with
+        @param room_jid (unicode): room JID or empty string to generate a unique name
+        """
+        profile = session_iface.ISATSession(self.session).profile
+        return self.sat_host.bridgeCall("radiocolLaunch", invited, room_jid, profile)
+
+    def jsonrpc_getEntitiesData(self, jids, keys):
+        """Get cached data for several entities at once
+
+        @param jids: list jids from who we wants data, or empty list for all jids in cache
+        @param keys: name of data we want (list)
+        @return: requested data"""
+        if not C.ALLOWED_ENTITY_DATA.issuperset(keys):
+            raise exceptions.PermissionError(
+                "Trying to access unallowed data (hack attempt ?)"
+            )
+        profile = session_iface.ISATSession(self.session).profile
+        try:
+            return self.sat_host.bridgeCall("getEntitiesData", jids, keys, profile)
+        except Exception as e:
+            raise failure.Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(e)))
+
+    def jsonrpc_getEntityData(self, jid, keys):
+        """Get cached data for an entity
+
+        @param jid: jid of contact from who we want data
+        @param keys: name of data we want (list)
+        @return: requested data"""
+        if not C.ALLOWED_ENTITY_DATA.issuperset(keys):
+            raise exceptions.PermissionError(
+                "Trying to access unallowed data (hack attempt ?)"
+            )
+        profile = session_iface.ISATSession(self.session).profile
+        try:
+            return self.sat_host.bridgeCall("getEntityData", jid, keys, profile)
+        except Exception as e:
+            raise failure.Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(e)))
+
+    def jsonrpc_getCard(self, jid_):
+        """Get VCard for entiry
+        @param jid_: jid of contact from who we want data
+        @return: id to retrieve the profile"""
+        profile = session_iface.ISATSession(self.session).profile
+        return self.sat_host.bridgeCall("getCard", jid_, profile)
+
+    @defer.inlineCallbacks
+    def jsonrpc_avatarGet(self, entity, cache_only, hash_only):
+        session_data = session_iface.ISATSession(self.session)
+        profile = session_data.profile
+        # profile_uuid = session_data.uuid
+        avatar = yield self.asyncBridgeCall(
+            "avatarGet", entity, cache_only, hash_only, profile
+        )
+        if hash_only:
+            defer.returnValue(avatar)
+        else:
+            filename = os.path.basename(avatar)
+            avatar_url = os.path.join(session_data.cache_dir, filename)
+            defer.returnValue(avatar_url)
+
+    def jsonrpc_getAccountDialogUI(self):
+        """Get the dialog for managing user account
+        @return: XML string of the XMLUI"""
+        profile = session_iface.ISATSession(self.session).profile
+        return self.sat_host.bridgeCall("getAccountDialogUI", profile)
+
+    def jsonrpc_getParamsUI(self):
+        """Return the parameters XML for profile"""
+        profile = session_iface.ISATSession(self.session).profile
+        return self.asyncBridgeCall("getParamsUI", C.SECURITY_LIMIT, C.APP_NAME, profile)
+
+    def jsonrpc_asyncGetParamA(self, param, category, attribute="value"):
+        """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
+            if param == "JabberID":
+                return self.asyncBridgeCall(
+                    "asyncGetParamA", param, category, attribute, profile_key=profile
+                )
+            elif param == "autoconnect":
+                return defer.succeed(C.BOOL_TRUE)
+        d = self.asyncBridgeCall(
+            "asyncGetParamA",
+            param,
+            category,
+            attribute,
+            C.SECURITY_LIMIT,
+            profile_key=profile,
+        )
+        return d
+
+    def jsonrpc_setParam(self, name, value, category):
+        profile = session_iface.ISATSession(self.session).profile
+        return self.sat_host.bridgeCall(
+            "setParam", name, value, category, C.SECURITY_LIMIT, profile
+        )
+
+    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
+        profile = session_iface.ISATSession(self.session).profile
+        d = self.asyncBridgeCall("launchAction", callback_id, data, profile)
+        return d
+
+    def jsonrpc_chatStateComposing(self, to_jid_s):
+        """Call the method to process a "composing" state.
+        @param to_jid_s: contact the user is composing to
+        """
+        profile = session_iface.ISATSession(self.session).profile
+        return self.sat_host.bridgeCall("chatStateComposing", to_jid_s, profile)
+
+    def jsonrpc_getNewAccountDomain(self):
+        """@return: the domain for new account creation"""
+        d = self.asyncBridgeCall("getNewAccountDomain")
+        return d
+
+    def jsonrpc_syntaxConvert(
+        self, text, syntax_from=C.SYNTAX_XHTML, syntax_to=C.SYNTAX_CURRENT
+    ):
+        """ Convert a text between two syntaxes
+        @param text: text to convert
+        @param syntax_from: source syntax (e.g. "markdown")
+        @param syntax_to: dest syntax (e.g.: "XHTML")
+        @param safe: clean resulting XHTML to avoid malicious code if True (forced here)
+        @return: converted text """
+        profile = session_iface.ISATSession(self.session).profile
+        return self.sat_host.bridgeCall(
+            "syntaxConvert", text, syntax_from, syntax_to, True, profile
+        )
+
+    def jsonrpc_getLastResource(self, jid_s):
+        """Get the last active resource of that contact."""
+        profile = session_iface.ISATSession(self.session).profile
+        return self.sat_host.bridgeCall("getLastResource", jid_s, profile)
+
+    def jsonrpc_getFeatures(self):
+        """Return the available features in the backend for profile"""
+        profile = session_iface.ISATSession(self.session).profile
+        return self.sat_host.bridgeCall("getFeatures", profile)
+
+    def jsonrpc_skipOTR(self):
+        """Tell the backend to leave OTR handling to Libervia."""
+        profile = session_iface.ISATSession(self.session).profile
+        return self.sat_host.bridgeCall("skipOTR", profile)
+
+    def jsonrpc_namespacesGet(self):
+        return self.sat_host.bridgeCall("namespacesGet")
+
+
+class WaitingRequests(dict):
+    def setRequest(self, request, profile, register_with_ext_jid=False):
+        """Add the given profile to the waiting list.
+
+        @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
+        """
+        dc = reactor.callLater(BRIDGE_TIMEOUT, self.purgeRequest, profile)
+        self[profile] = (request, dc, register_with_ext_jid)
+
+    def purgeRequest(self, profile):
+        """Remove the given profile from the waiting list.
+
+        @param profile (str): %(doc_profile)s
+        """
+        try:
+            dc = self[profile][1]
+        except KeyError:
+            return
+        if dc.active():
+            dc.cancel()
+        del self[profile]
+
+    def getRequest(self, profile):
+        """Get the waiting request for the given profile.
+
+        @param profile (str): %(doc_profile)s
+        @return: the waiting request or None
+        """
+        return self[profile][0] if profile in self else None
+
+    def getRegisterWithExtJid(self, profile):
+        """Get the value of the register_with_ext_jid parameter.
+
+        @param profile (str): %(doc_profile)s
+        @return: bool or None
+        """
+        return self[profile][2] if profile in self else None
+
+
+class Register(JSONRPCMethodManager):
+    """This class manage the registration procedure with SàT
+    It provide an api for the browser, check password and setup the web server"""
+
+    def __init__(self, sat_host):
+        JSONRPCMethodManager.__init__(self, sat_host)
+        self.profiles_waiting = {}
+        self.request = None
+
+    def render(self, request):
+        """
+        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
+        """
+        if request.postpath == ["login"]:
+            return self.loginOrRegister(request)
+        _session = request.getSession()
+        parsed = jsonrpclib.loads(request.content.read())
+        method = parsed.get("method")  # pylint: disable=E1103
+        if method not in ["getSessionMetadata", "registerParams", "menusGet"]:
+            # if we don't call these methods, we need to be identified
+            profile = session_iface.ISATSession(_session).profile
+            if not profile:
+                # user is not identified, we return a jsonrpc fault
+                fault = jsonrpclib.Fault(
+                    C.ERRNUM_LIBERVIA, C.NOT_ALLOWED
+                )  # FIXME: define some standard error codes for libervia
+                return jsonrpc.JSONRPC._cbRender(
+                    self, fault, request, parsed.get("id"), parsed.get("jsonrpc")
+                )  # pylint: disable=E1103
+        self.request = request
+        return jsonrpc.JSONRPC.render(self, request)
+
+    def loginOrRegister(self, request):
+        """This method is called with the POST information from the registering form.
+
+        @param request: request of the register form
+        @return: a constant indicating the state:
+            - C.BAD_REQUEST: something is wrong in the request (bad arguments)
+            - a return value from self._loginAccount or self._registerNewAccount
+        """
+        try:
+            submit_type = request.args["submit_type"][0]
+        except KeyError:
+            return C.BAD_REQUEST
+
+        if submit_type == "register":
+            self._registerNewAccount(request)
+            return server.NOT_DONE_YET
+        elif submit_type == "login":
+            self._loginAccount(request)
+            return server.NOT_DONE_YET
+        return Exception("Unknown submit type")
+
+    @defer.inlineCallbacks
+    def _registerNewAccount(self, request):
+        try:
+            login = request.args["register_login"][0]
+            password = request.args["register_password"][0]
+            email = request.args["email"][0]
+        except KeyError:
+            request.write(C.BAD_REQUEST)
+            request.finish()
+            return
+        status = yield self.sat_host.registerNewAccount(request, login, password, email)
+        request.write(status)
+        request.finish()
+
+    @defer.inlineCallbacks
+    def _loginAccount(self, request):
+        """Try to authenticate the user with the request information.
+
+        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.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.NOT_CONNECTED: connection has not been established
+        the request will then be finished
+        @param request: request of the register form
+        """
+        try:
+            login = request.args["login"][0]
+            password = request.args["login_password"][0]
+        except KeyError:
+            request.write(C.BAD_REQUEST)
+            request.finish()
+            return
+
+        assert login
+
+        try:
+            status = yield self.sat_host.connect(request, login, password)
+        except (
+            exceptions.DataError,
+            exceptions.ProfileUnknownError,
+            exceptions.PermissionError,
+        ):
+            request.write(C.PROFILE_AUTH_ERROR)
+            request.finish()
+            return
+        except exceptions.NotReady:
+            request.write(C.ALREADY_WAITING)
+            request.finish()
+            return
+        except exceptions.TimeOutError:
+            request.write(C.NO_REPLY)
+            request.finish()
+            return
+        except exceptions.InternalError as e:
+            request.write(e.message)
+            request.finish()
+            return
+        except exceptions.ConflictError:
+            request.write(C.SESSION_ACTIVE)
+            request.finish()
+            return
+        except ValueError as e:
+            if e.message in (C.PROFILE_AUTH_ERROR, C.XMPP_AUTH_ERROR):
+                request.write(e.message)
+                request.finish()
+                return
+            else:
+                raise e
+
+        assert status
+        request.write(status)
+        request.finish()
+
+    def jsonrpc_isConnected(self):
+        _session = self.request.getSession()
+        profile = session_iface.ISATSession(_session).profile
+        return self.sat_host.bridgeCall("isConnected", profile)
+
+    def jsonrpc_connect(self):
+        _session = self.request.getSession()
+        profile = session_iface.ISATSession(_session).profile
+        if self.waiting_profiles.getRequest(profile):
+            raise jsonrpclib.Fault(
+                1, C.ALREADY_WAITING
+            )  # FIXME: define some standard error codes for libervia
+        self.waiting_profiles.setRequest(self.request, profile)
+        self.sat_host.bridgeCall("connect", profile)
+        return server.NOT_DONE_YET
+
+    def jsonrpc_getSessionMetadata(self):
+        """Return metadata useful on session start
+
+        @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
+            "allow_registration" (bool): True if registration is allowed
+                this key is only present if profile is unplugged
+        @return: a couple (registered, message) with:
+        - registered:
+        - message:
+        """
+        metadata = {}
+        _session = self.request.getSession()
+        profile = session_iface.ISATSession(_session).profile
+        if profile:
+            metadata["plugged"] = True
+        else:
+            metadata["plugged"] = False
+            metadata["warning"] = self._getSecurityWarning()
+            metadata["allow_registration"] = self.sat_host.options["allow_registration"]
+        return metadata
+
+    def jsonrpc_registerParams(self):
+        """Register the frontend specific parameters"""
+        # params = """<params><individual>...</category></individual>"""
+        # self.sat_host.bridge.paramsRegisterApp(params, C.SECURITY_LIMIT, C.APP_NAME)
+
+    def jsonrpc_menusGet(self):
+        """Return the parameters XML for profile"""
+        # XXX: we put this method in Register because we get menus before being logged
+        return self.sat_host.bridgeCall("menusGet", "", C.SECURITY_LIMIT)
+
+    def _getSecurityWarning(self):
+        """@return: a security warning message, or None if the connection is secure"""
+        if (
+            self.request.URLPath().scheme == "https"
+            or not self.sat_host.options["security_warning"]
+        ):
+            return None
+        text = (
+            "<p>"
+            + D_("You are about to connect to an unsecure service.")
+            + "</p><p>&nbsp;</p><p>"
+        )
+
+        if self.sat_host.options["connection_type"] == "both":
+            new_port = (
+                (":%s" % self.sat_host.options["port_https_ext"])
+                if self.sat_host.options["port_https_ext"] != HTTPS_PORT
+                else ""
+            )
+            url = "https://%s" % self.request.URLPath().netloc.replace(
+                ":%s" % self.sat_host.options["port"], new_port
+            )
+            text += D_(
+                "Please read our %(faq_prefix)ssecurity notice%(faq_suffix)s regarding HTTPS"
+            ) % {
+                "faq_prefix": '<a href="http://salut-a-toi.org/faq.html#https" target="#">',
+                "faq_suffix": "</a>",
+            }
+            text += "</p><p>" + D_("and use the secure version of this website:")
+            text += '</p><p>&nbsp;</p><p align="center"><a href="%(url)s">%(url)s</a>' % {
+                "url": url
+            }
+        else:
+            text += D_("You should ask your administrator to turn on HTTPS.")
+
+        return text + "</p><p>&nbsp;</p>"
+
+
+class SignalHandler(jsonrpc.JSONRPC):
+    def __init__(self, sat_host):
+        web_resource.Resource.__init__(self)
+        self.register = None
+        self.sat_host = sat_host
+        self._last_service_prof_disconnect = time.time()
+        self.signalDeferred = {}  # dict of deferred (key: profile, value: Deferred)
+        # which manages the long polling HTTP request with signals
+        self.queue = {}
+
+    def plugRegister(self, register):
+        self.register = register
+
+    def jsonrpc_getSignals(self):
+        """Keep the connection alive until a signal is received, then send it
+        @return: (signal, *signal_args)"""
+        _session = self.request.getSession()
+        profile = session_iface.ISATSession(_session).profile
+        if profile in self.queue:  # if we have signals to send in queue
+            if self.queue[profile]:
+                return self.queue[profile].pop(0)
+            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
+
+        def unlock(signal, profile):
+            _session.unlock()
+            try:
+                source_defer = self.signalDeferred[profile]
+                if source_defer.called and source_defer.result[0] == "disconnected":
+                    log.info(u"[%s] disconnected" % (profile,))
+                    try:
+                        _session.expire()
+                    except KeyError:
+                        #  FIXME: happen if session is ended using login page
+                        #        when pyjamas page is also launched
+                        log.warning(u"session is already expired")
+            except IndexError:
+                log.error("Deferred result should be a tuple with fonction name first")
+
+        self.signalDeferred[profile] = defer.Deferred()
+        self.request.notifyFinish().addBoth(unlock, profile)
+        return self.signalDeferred[profile]
+
+    def getGenericCb(self, function_name):
+        """Return a generic function which send all params to signalDeferred.callback
+        function must have profile as last argument"""
+
+        def genericCb(*args):
+            profile = args[-1]
+            if not profile in self.sat_host.prof_connected:
+                return
+            signal_data = (function_name, args[:-1])
+            try:
+                signal_callback = self.signalDeferred[profile].callback
+            except KeyError:
+                self.queue.setdefault(profile, []).append(signal_data)
+            else:
+                signal_callback(signal_data)
+                del self.signalDeferred[profile]
+
+        return genericCb
+
+    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
+        @param action_data(dict): see bridge documentation
+        @param action_id(unicode): identitifer of the action
+        @param security_limit(int): %(doc_security_limit)s
+        @param profile(unicode): %(doc_profile)s
+        """
+        if not profile in self.sat_host.prof_connected:
+            return
+        # FIXME: manage security limit in a dedicated method
+        #        raise an exception if it's not OK
+        #        and read value in sat.conf
+        if security_limit >= C.SECURITY_LIMIT:
+            log.debug(
+                u"Ignoring action  {action_id}, blocked by security limit".format(
+                    action_id=action_id
+                )
+            )
+            return
+        signal_data = ("actionNew", (action_data, action_id, security_limit))
+        try:
+            signal_callback = self.signalDeferred[profile].callback
+        except KeyError:
+            self.queue.setdefault(profile, []).append(signal_data)
+        else:
+            signal_callback(signal_data)
+            del self.signalDeferred[profile]
+
+    def connected(self, profile, jid_s):
+        """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.
+        """
+        #  FIXME: _logged should not be called from here, check this code
+        #  FIXME: check if needed to connect with external jid
+        # jid_s is handled in QuickApp.connectionHandler already
+        # assert self.register  # register must be plugged
+        # request = self.sat_host.waiting_profiles.getRequest(profile)
+        # if request:
+        #     self.sat_host._logged(profile, request)
+
+    def disconnected(self, profile):
+        if profile == C.SERVICE_PROFILE:
+            # if service profile has been disconnected, we try to reconnect it
+            # if we can't we show error message
+            # and if we have 2 disconnection in a short time, we don't try to reconnect
+            # and display an error message
+            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"
+                    )
+                )
+            else:
+                log.info(
+                    _(
+                        u"Service profile has been disconnected, but we need it! 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_)
+                    )
+                )
+            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)
+            )
+            return
+        self.sat_host.prof_connected.remove(profile)
+        if profile in self.signalDeferred:
+            self.signalDeferred[profile].callback(("disconnected",))
+            del self.signalDeferred[profile]
+        else:
+            if profile not in self.queue:
+                self.queue[profile] = []
+            self.queue[profile].append(("disconnected",))
+
+    def render(self, request):
+        """
+        Render method wich reject access if user is not identified
+        """
+        _session = request.getSession()
+        parsed = jsonrpclib.loads(request.content.read())
+        profile = session_iface.ISATSession(_session).profile
+        if not profile:
+            # user is not identified, we return a jsonrpc fault
+            fault = jsonrpclib.Fault(
+                C.ERRNUM_LIBERVIA, C.NOT_ALLOWED
+            )  # FIXME: define some standard error codes for libervia
+            return jsonrpc.JSONRPC._cbRender(
+                self, fault, request, parsed.get("id"), parsed.get("jsonrpc")
+            )  # pylint: disable=E1103
+        self.request = request
+        return jsonrpc.JSONRPC.render(self, request)
+
+
+class UploadManager(web_resource.Resource):
+    """This class manage the upload of a file
+    It redirect the stream to SàT core backend"""
+
+    isLeaf = True
+    NAME = "path"  # name use by the FileUpload
+
+    def __init__(self, sat_host):
+        self.sat_host = sat_host
+        self.upload_dir = tempfile.mkdtemp()
+        self.sat_host.addCleanup(shutil.rmtree, self.upload_dir)
+
+    def getTmpDir(self):
+        return self.upload_dir
+
+    def _getFileName(self, request):
+        """Generate unique filename for a file"""
+        raise NotImplementedError
+
+    def _fileWritten(self, request, filepath):
+        """Called once the file is actually written on disk
+        @param request: HTTP request object
+        @param filepath: full filepath on the server
+        @return: a tuple with the name of the async bridge method
+        to be called followed by its arguments.
+        """
+        raise NotImplementedError
+
+    def render(self, request):
+        """
+        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
+        """
+        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)
+
+        with open(filepath, "w") as f:
+            f.write(request.args[self.NAME][0])
+
+        def finish(d):
+            error = isinstance(d, Exception) or isinstance(d, failure.Failure)
+            request.write(C.UPLOAD_KO if error else C.UPLOAD_OK)
+            # TODO: would be great to re-use the original Exception class and message
+            # but it is lost in the middle of the backtrace and encapsulated within
+            # a DBusException instance --> extract the data from the backtrace?
+            request.finish()
+
+        d = JSONRPCMethodManager(self.sat_host).asyncBridgeCall(
+            *self._fileWritten(request, filepath)
+        )
+        d.addCallbacks(lambda d: finish(d), lambda failure: finish(failure))
+        return server.NOT_DONE_YET
+
+
+class UploadManagerRadioCol(UploadManager):
+    NAME = "song"
+
+    def _getFileName(self, request):
+        extension = os.path.splitext(request.args["filename"][0])[1]
+        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
+
+    def _fileWritten(self, request, filepath):
+        """Called once the file is actually written on disk
+        @param request: HTTP request object
+        @param filepath: full filepath on the server
+        @return: a tuple with the name of the async bridge method
+        to be called followed by its arguments.
+        """
+        profile = session_iface.ISATSession(request.getSession()).profile
+        return ("radiocolSongAdded", request.args["referee"][0], filepath, profile)
+
+
+class UploadManagerAvatar(UploadManager):
+    NAME = "avatar_path"
+
+    def _getFileName(self, request):
+        return str(uuid.uuid4())
+
+    def _fileWritten(self, request, filepath):
+        """Called once the file is actually written on disk
+        @param request: HTTP request object
+        @param filepath: full filepath on the server
+        @return: a tuple with the name of the async bridge method
+        to be called followed by its arguments.
+        """
+        profile = session_iface.ISATSession(request.getSession()).profile
+        return ("setAvatar", filepath, profile)
+
+
+class Libervia(service.Service):
+    debug = defer.Deferred.debug  # True if twistd/Libervia is launched in debug mode
+
+    def __init__(self, options):
+        self.options = options
+        self.initialised = defer.Deferred()
+        self.waiting_profiles = WaitingRequests()  # FIXME: should be removed
+
+        if self.options["base_url_ext"]:
+            self.base_url_ext = self.options.pop("base_url_ext")
+            if self.base_url_ext[-1] != "/":
+                self.base_url_ext += "/"
+            self.base_url_ext_data = urlparse.urlsplit(self.base_url_ext)
+        else:
+            self.base_url_ext = None
+            # we split empty string anyway so we can do things like
+            # scheme = self.base_url_ext_data.scheme or 'https'
+            self.base_url_ext_data = urlparse.urlsplit("")
+
+        if not self.options["port_https_ext"]:
+            self.options["port_https_ext"] = self.options["port_https"]
+        if self.options["data_dir"] == DATA_DIR_DEFAULT:
+            coerceDataDir(
+                self.options["data_dir"]
+            )  # this is not done when using the default value
+
+        self.html_dir = os.path.join(self.options["data_dir"], C.HTML_DIR)
+        self.themes_dir = os.path.join(self.options["data_dir"], C.THEMES_DIR)
+
+        self._cleanup = []
+
+        self.signal_handler = SignalHandler(self)
+        self.sessions = {}  # key = session value = user
+        self.prof_connected = set()  # Profiles connected
+        self.ns_map = {}  # map of short name to namespaces
+
+        ## bridge ##
+        try:
+            self.bridge = Bridge()
+        except BridgeExceptionNoService:
+            print(u"Can't connect to SàT backend, are you sure it's launched ?")
+            sys.exit(1)
+        self.bridge.bridgeConnect(callback=self._bridgeCb, errback=self._bridgeEb)
+
+    def _namespacesGetCb(self, ns_map):
+        self.ns_map = ns_map
+
+    def _namespacesGetEb(self, failure_):
+        log.error(_(u"Can't get namespaces map: {msg}").format(msg=failure_))
+
+    def backendReady(self, dummy):
+        self.root = root = LiberviaRootResource(self.html_dir)
+        _register = Register(self)
+        _upload_radiocol = UploadManagerRadioCol(self)
+        _upload_avatar = UploadManagerAvatar(self)
+        d = self.bridgeCall("namespacesGet")
+        d.addCallback(self._namespacesGetCb)
+        d.addErrback(self._namespacesGetEb)
+        self.signal_handler.plugRegister(_register)
+        self.bridge.register_signal("connected", self.signal_handler.connected)
+        self.bridge.register_signal("disconnected", self.signal_handler.disconnected)
+        # core
+        for signal_name in [
+            "presenceUpdate",
+            "messageNew",
+            "subscribe",
+            "contactDeleted",
+            "newContact",
+            "entityDataUpdated",
+            "paramUpdate",
+        ]:
+            self.bridge.register_signal(
+                signal_name, self.signal_handler.getGenericCb(signal_name)
+            )
+        # 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 [
+            "psEvent",
+            "mucRoomJoined",
+            "tarotGameStarted",
+            "tarotGameNew",
+            "tarotGameChooseContrat",
+            "tarotGameShowCards",
+            "tarotGameInvalidCards",
+            "tarotGameCardsPlayed",
+            "tarotGameYourTurn",
+            "tarotGameScore",
+            "tarotGamePlayers",
+            "radiocolStarted",
+            "radiocolPreload",
+            "radiocolPlay",
+            "radiocolNoUpload",
+            "radiocolUploadOk",
+            "radiocolSongRejected",
+            "radiocolPlayers",
+            "mucRoomLeft",
+            "mucRoomUserChangedNick",
+            "chatStateReceived",
+        ]:
+            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)
+
+        # files upload
+        self.putChild("upload_radiocol", _upload_radiocol)
+        self.putChild("upload_avatar", _upload_avatar)
+
+        # static pages
+        self.putChild("blog_legacy", MicroBlog(self))
+        self.putChild(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)
+        if self.options["connection_type"] in ("http", "both"):
+            ws = websockets.LiberviaPageWSProtocol.getResource(self, secure=False)
+            self.putChild("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"
+        )
+        self.bridge.register_signal(
+            "messageNew", partial(LiberviaPage.onSignal, self, "messageNew")
+        )
+
+        #  Progress handling
+        self.bridge.register_signal(
+            "progressStarted", partial(ProgressHandler._signal, "started")
+        )
+        self.bridge.register_signal(
+            "progressFinished", partial(ProgressHandler._signal, "finished")
+        )
+        self.bridge.register_signal(
+            "progressError", partial(ProgressHandler._signal, "error")
+        )
+
+        # 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.cache_resource = web_resource.NoResource()
+        self.putChild(C.CACHE_DIR, self.cache_resource)
+
+        # special
+        self.putChild(
+            "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
+        # pyjamas tests, redirected only for dev versions
+        if self.version[-1] == "D":
+            self.putChild("test", web_util.Redirect("/libervia_test.html"))
+
+        # redirections
+        root._initRedirections(self.options)
+
+        server.Request.defaultContentType = "text/html; charset=utf-8"
+        wrapped = web_resource.EncodingResourceWrapper(
+            root, [server.GzipEncoderFactory()]
+        )
+        self.site = server.Site(wrapped)
+        self.site.sessionFactory = LiberviaSession
+        self.renderer = template.Renderer(self)
+        self.putChild("templates", ProtectedFile(self.renderer.base_dir))
+
+    def initEb(self, failure):
+        log.error(_(u"Init error: {msg}").format(msg=failure))
+        reactor.stop()
+        return failure
+
+    def _bridgeCb(self):
+        self.bridge.getReady(
+            lambda: self.initialised.callback(None),
+            lambda failure: self.initialised.errback(Exception(failure)),
+        )
+        self.initialised.addCallback(self.backendReady)
+        self.initialised.addErrback(self.initEb)
+
+    def _bridgeEb(self, failure):
+        log.error(u"Can't connect to bridge: {}".format(failure))
+
+    @property
+    def version(self):
+        """Return the short version of Libervia"""
+        return C.APP_VERSION
+
+    @property
+    def full_version(self):
+        """Return the full version of Libervia (with extra data when in development mode)"""
+        version = self.version
+        if version[-1] == "D":
+            # we are in debug version, we add extra data
+            try:
+                return self._version_cache
+            except AttributeError:
+                self._version_cache = u"{} ({})".format(
+                    version, utils.getRepositoryData(libervia)
+                )
+                return self._version_cache
+        else:
+            return version
+
+    def bridgeCall(self, method_name, *args, **kwargs):
+        """Call an asynchronous bridge method and return a deferred
+
+        @param method_name: name of the method as a unicode
+        @return: a deferred which trigger the result
+
+        """
+        d = defer.Deferred()
+
+        def _callback(*args):
+            if not args:
+                d.callback(None)
+            else:
+                if len(args) != 1:
+                    Exception("Multiple return arguments not supported")
+                d.callback(args[0])
+
+        def _errback(result):
+            d.errback(
+                failure.Failure(
+                    jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, result.classname)
+                )
+            )
+
+        kwargs["callback"] = _callback
+        kwargs["errback"] = _errback
+        getattr(self.bridge, method_name)(*args, **kwargs)
+        return d
+
+    @defer.inlineCallbacks
+    def _logged(self, profile, request):
+        """Set everything when a user just logged in
+
+        @param profile
+        @param request
+        @return: a constant indicating the state:
+            - C.PROFILE_LOGGED
+            - C.PROFILE_LOGGED_EXT_JID
+        @raise exceptions.ConflictError: session is already active
+        """
+        register_with_ext_jid = self.waiting_profiles.getRegisterWithExtJid(profile)
+        self.waiting_profiles.purgeRequest(profile)
+        session = request.getSession()
+        sat_session = session_iface.ISATSession(session)
+        if sat_session.profile:
+            log.error(_(u"/!\\ Session has already a profile, this should NEVER happen!"))
+            raise failure.Failure(exceptions.ConflictError("Already active"))
+
+        sat_session.profile = profile
+        self.prof_connected.add(profile)
+        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
+        self.cache_resource.putChild(sat_session.uuid, ProtectedFile(cache_dir))
+        log.debug(
+            _(u"profile cache resource added from {uuid} to {path}").format(
+                uuid=sat_session.uuid, path=cache_dir
+            )
+        )
+
+        def onExpire():
+            log.info(u"Session expired (profile={profile})".format(profile=profile))
+            self.cache_resource.delEntity(sat_session.uuid)
+            log.debug(
+                _(u"profile cache resource {uuid} deleted").format(uuid=sat_session.uuid)
+            )
+            try:
+                # We purge the queue
+                del self.signal_handler.queue[profile]
+            except KeyError:
+                pass
+            # and now we disconnect the profile
+            self.bridgeCall("disconnect", profile)
+
+        session.notifyOnExpire(onExpire)
+
+        # FIXME: those session infos should be returned by connect or isConnected
+        infos = yield self.bridgeCall("sessionInfosGet", profile)
+        sat_session.jid = jid.JID(infos["jid"])
+        sat_session.backend_started = int(infos["started"])
+
+        state = C.PROFILE_LOGGED_EXT_JID if register_with_ext_jid else C.PROFILE_LOGGED
+        defer.returnValue(state)
+
+    @defer.inlineCallbacks
+    def connect(self, request, login, password):
+        """log user in
+
+        If an other user was already logged, it will be unlogged first
+        @param request(server.Request): request linked to the session
+        @param login(unicode): user login
+            can be profile name
+            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
+        @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.NotReady: a profile connection is already waiting
+        @raise exceptions.TimeoutError: didn't received and answer from Bridge
+        @raise exceptions.InternalError: unknown error
+        @raise ValueError(C.PROFILE_AUTH_ERROR): invalid login and/or password
+        @raise ValueError(C.XMPP_AUTH_ERROR): invalid XMPP account password
+        """
+
+        # XXX: all security checks must be done here, even if present in javascript
+        if login.startswith("@"):
+            raise failure.Failure(exceptions.DataError("No profile_key allowed"))
+
+        if login.startswith("guest@@") and login.count("@") == 2:
+            log.debug("logging a guest account")
+        elif "@" in login:
+            if login.count("@") != 1:
+                raise failure.Failure(
+                    exceptions.DataError("Invalid login: {login}".format(login=login))
+                )
+            try:
+                login_jid = jid.JID(login)
+            except (RuntimeError, jid.InvalidFormat, AttributeError):
+                raise failure.Failure(exceptions.DataError("No profile_key allowed"))
+
+            # FIXME: should it be cached?
+            new_account_domain = yield self.bridgeCall("getNewAccountDomain")
+
+            if login_jid.host == new_account_domain:
+                # redirect "user@libervia.org" to the "user" profile
+                login = login_jid.user
+                login_jid = None
+        else:
+            login_jid = None
+
+        try:
+            profile = yield self.bridgeCall("profileNameGet", login)
+        except Exception:  # XXX: ProfileUnknownError wouldn't work, it's encapsulated
+            # FIXME: find a better way to handle bridge errors
+            if (
+                login_jid is not None and login_jid.user
+            ):  # 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"
+                    )
+                    raise failure.Failure(
+                        exceptions.DataError(
+                            u"JID login while registration is not allowed"
+                        )
+                    )
+                profile = login  # FIXME: what if there is a resource?
+                connect_method = "asyncConnectWithXMPPCredentials"
+                register_with_ext_jid = True
+            else:  # non existing username
+                raise failure.Failure(exceptions.ProfileUnknownError())
+        else:
+            if profile != login or (
+                not password
+                and profile
+                not in self.options["empty_password_allowed_warning_dangerous_list"]
+            ):
+                # profiles with empty passwords are restricted to local frontends
+                raise exceptions.PermissionError
+            register_with_ext_jid = False
+
+            connect_method = "connect"
+
+        # we check if there is not already an active session
+        sat_session = session_iface.ISATSession(request.getSession())
+        if sat_session.profile:
+            # 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)
+                )
+                self.purgeSession(request)
+
+        if self.waiting_profiles.getRequest(profile):
+            #  FIXME: check if and when this can happen
+            raise failure.Failure(exceptions.NotReady("Already waiting"))
+
+        self.waiting_profiles.setRequest(request, profile, register_with_ext_jid)
+        try:
+            connected = yield self.bridgeCall(connect_method, profile, password)
+        except Exception as failure_:
+            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
+                    )
+                )
+                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
+                    )
+                )
+                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)"
+                    )
+                )
+                raise exceptions.TimeOutError
+            else:
+                log.error(
+                    u'Unmanaged fault string "{fault}" in errback for the connection of profile {profile}'.format(
+                        fault=fault, profile=profile
+                    )
+                )
+                raise failure.Failure(exceptions.InternalError(fault))
+
+        if connected:
+            #  profile is already connected in backend
+            # do we have a corresponding session in Libervia?
+            sat_session = session_iface.ISATSession(request.getSession())
+            if sat_session.profile:
+                # yes, session is active
+                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)
+                    )
+                    raise exceptions.InternalError("profile mismatch")
+                defer.returnValue(C.SESSION_ACTIVE)
+            log.info(
+                _(
+                    u"profile {profile} was already connected in backend".format(
+                        profile=profile
+                    )
+                )
+            )
+            #  no, we have to create it
+
+        state = yield self._logged(profile, request)
+        defer.returnValue(state)
+
+    def registerNewAccount(self, request, login, password, email):
+        """Create a new account, or return error
+        @param request(server.Request): request linked to the session
+        @param login(unicode): new account requested login
+        @param email(unicode): new account email
+        @param password(unicode): new account password
+        @return(unicode): a constant indicating the state:
+            - C.BAD_REQUEST: something is wrong in the request (bad arguments)
+            - C.INVALID_INPUT: one of the data is not valid
+            - C.REGISTRATION_SUCCEED: new account has been successfully registered
+            - C.ALREADY_EXISTS: the given profile already exists
+            - C.INTERNAL_ERROR or any unmanaged fault string
+        @raise PermissionError: registration is now allowed in server configuration
+        """
+        if not self.options["allow_registration"]:
+            log.warning(
+                _(u"Registration received while it is not allowed, hack attempt?")
+            )
+            raise failure.Failure(
+                exceptions.PermissionError(u"Registration is not allowed on this server")
+            )
+
+        if (
+            not re.match(C.REG_LOGIN_RE, login)
+            or not re.match(C.REG_EMAIL_RE, email, re.IGNORECASE)
+            or len(password) < C.PASSWORD_MIN_LENGTH
+        ):
+            return C.INVALID_INPUT
+
+        def registered(result):
+            return C.REGISTRATION_SUCCEED
+
+        def registeringError(failure):
+            status = failure.value.faultString
+            if status == "ConflictError":
+                return C.ALREADY_EXISTS
+            elif status == "InternalError":
+                return C.INTERNAL_ERROR
+            else:
+                log.error(
+                    _(u"Unknown registering error status: {status }").format(
+                        status=status
+                    )
+                )
+                return status
+
+        d = self.bridgeCall("registerSatAccount", email, password, login)
+        d.addCallback(registered)
+        d.addErrback(registeringError)
+        return d
+
+    def addCleanup(self, callback, *args, **kwargs):
+        """Add cleaning method to call when service is stopped
+
+        cleaning method will be called in reverse order of they insertion
+        @param callback: callable to call on service stop
+        @param *args: list of arguments of the callback
+        @param **kwargs: list of keyword arguments of the callback"""
+        self._cleanup.insert(0, (callback, args, kwargs))
+
+    def startService(self):
+        """Connect the profile for Libervia and start the HTTP(S) server(s)"""
+
+        def eb(e):
+            log.error(_(u"Connection failed: %s") % e)
+            self.stop()
+
+        def initOk(dummy):
+            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
+                    )
+                )
+                self.stop()
+                return
+            if not connected:
+                self.bridge.connect(
+                    C.SERVICE_PROFILE,
+                    self.options["passphrase"],
+                    {},
+                    callback=self._startService,
+                    errback=eb,
+                )
+            else:
+                self._startService()
+
+        self.initialised.addCallback(initOk)
+
+    ## URLs ##
+
+    def putChild(self, path, resource):
+        """Add a child to the root 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()]),
+        )
+
+    def getExtBaseURLData(self, request):
+        """Retrieve external base URL Data
+
+        this method tried to retrieve the base URL found by external user
+        It does by checking in this order:
+            - 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
+        """
+        ext_data = self.base_url_ext_data
+        url_path = request.URLPath()
+        if not ext_data.scheme or not ext_data.netloc:
+            #  ext_data is not specified, we check headers
+            if request.requestHeaders.hasHeader("x-forwarded-host"):
+                # we are behing a proxy
+                # we fill proxy_scheme and proxy_netloc value
+                proxy_host = request.requestHeaders.getRawHeaders("x-forwarded-host")[0]
+                try:
+                    proxy_server = request.requestHeaders.getRawHeaders(
+                        "x-forwarded-server"
+                    )[0]
+                except TypeError:
+                    # no x-forwarded-server found, we use proxy_host
+                    proxy_netloc = proxy_host
+                else:
+                    # if the proxy host has a port, we use it with server name
+                    proxy_port = urlparse.urlsplit(u"//{}".format(proxy_host)).port
+                    proxy_netloc = (
+                        u"{}:{}".format(proxy_server, proxy_port)
+                        if proxy_port is not None
+                        else proxy_server
+                    )
+                proxy_netloc = proxy_netloc.decode("utf-8")
+                try:
+                    proxy_scheme = request.requestHeaders.getRawHeaders(
+                        "x-forwarded-proto"
+                    )[0].decode("utf-8")
+                except TypeError:
+                    proxy_scheme = None
+            else:
+                proxy_scheme, proxy_netloc = None, None
+        else:
+            proxy_scheme, proxy_netloc = None, None
+
+        return urlparse.SplitResult(
+            ext_data.scheme or proxy_scheme or url_path.scheme.decode("utf-8"),
+            ext_data.netloc or proxy_netloc or url_path.netloc.decode("utf-8"),
+            ext_data.path or u"/",
+            "",
+            "",
+        )
+
+    def getExtBaseURL(self, request, path="", query="", fragment="", scheme=None):
+        """Get external URL according to given elements
+
+        external URL is the URL seen by external user
+        @param path(unicode): same as for urlsplit.urlsplit
+            path will be prefixed to follow found external URL if suitable
+        @param params(unicode): same as for urlsplit.urlsplit
+        @param query(unicode): same as for urlsplit.urlsplit
+        @param fragment(unicode): same as for urlsplit.urlsplit
+        @param scheme(unicode, None): if not None, will override scheme from base URL
+        @return (unicode): external URL
+        """
+        split_result = self.getExtBaseURLData(request)
+        return urlparse.urlunsplit(
+            (
+                split_result.scheme.decode("utf-8") if scheme is None else scheme,
+                split_result.netloc.decode("utf-8"),
+                os.path.join(split_result.path, path),
+                query,
+                fragment,
+            )
+        )
+
+    def checkRedirection(self, url):
+        """check is a part of the URL prefix is redirected then replace it
+
+        @param url(unicode): url to check
+        @return (unicode): possibly redirected URL which should link to the same location
+        """
+        inv_redirections = self.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])
+            if test_url in inv_redirections:
+                rem_url = url_parts[idx:]
+                return os.path.join(
+                    u"/", u"/".join([inv_redirections[test_url]] + rem_url)
+                )
+        return url
+
+    ## Sessions ##
+
+    def purgeSession(self, request):
+        """helper method to purge a session during request handling"""
+        session = request.session
+        if session is not None:
+            log.debug(_(u"session purge"))
+            session.expire()
+            # FIXME: not clean but it seems that it's the best way to reset
+            #        session during request handling
+            request._secureSession = request._insecureSession = None
+
+    def getSessionData(self, request, *args):
+        """helper method to retrieve session data
+
+        @param request(server.Request): request linked to the session
+        @param *args(zope.interface.Interface): interface of the session to get
+        @return (iterator(data)): requested session data
+        """
+        session = request.getSession()
+        if len(args) == 1:
+            return args[0](session)
+        else:
+            return (iface(session) for iface in args)
+
+    @defer.inlineCallbacks
+    def getAffiliation(self, request, service, node):
+        """retrieve pubsub node affiliation for current user
+
+        use cache first, and request pubsub service if not cache is found
+        @param request(server.Request): request linked to the session
+        @param service(jid.JID): pubsub service
+        @param node(unicode): pubsub node
+        @return (unicode): affiliation
+        """
+        sat_session = self.getSessionData(request, session_iface.ISATSession)
+        if sat_session.profile is None:
+            raise exceptions.InternalError(u"profile must be set to use this method")
+        affiliation = sat_session.getAffiliation(service, node)
+        if affiliation is not None:
+            defer.returnValue(affiliation)
+        else:
+            try:
+                affiliations = yield self.bridgeCall(
+                    "psAffiliationsGet", service.full(), node, sat_session.profile
+                )
+            except Exception as e:
+                log.warning(
+                    "Can't retrieve affiliation for {service}/{node}: {reason}".format(
+                        service=service, node=node, reason=e
+                    )
+                )
+                affiliation = u""
+            else:
+                try:
+                    affiliation = affiliations[node]
+                except KeyError:
+                    affiliation = u""
+            sat_session.setAffiliation(service, node, affiliation)
+            defer.returnValue(affiliation)
+
+    ## Websocket (dynamic pages) ##
+
+    def getWebsocketURL(self, request):
+        base_url_split = self.getExtBaseURLData(request)
+        if base_url_split.scheme.endswith("s"):
+            scheme = u"wss"
+        else:
+            scheme = u"ws"
+
+        return self.getExtBaseURL(request, path=scheme, scheme=scheme)
+
+    def registerWSToken(self, token, page, request):
+        websockets.LiberviaPageWSProtocol.registerToken(token, page, request)
+
+    ## Various utils ##
+
+    def getHTTPDate(self, timestamp=None):
+        now = time.gmtime(timestamp)
+        fmt_date = u"{day_name}, %d {month_name} %Y %H:%M:%S GMT".format(
+            day_name=C.HTTP_DAYS[now.tm_wday], month_name=C.HTTP_MONTH[now.tm_mon - 1]
+        )
+        return time.strftime(fmt_date, now)
+
+    ## TLS related methods ##
+
+    def _TLSOptionsCheck(self):
+        """Check options coherence if TLS is activated, and update missing values
+
+        Must be called only if TLS is activated
+        """
+        if not self.options["tls_certificate"]:
+            log.error(u"a TLS certificate is needed to activate HTTPS connection")
+            self.quit(1)
+        if not self.options["tls_private_key"]:
+            self.options["tls_private_key"] = self.options["tls_certificate"]
+
+        if not self.options["tls_private_key"]:
+            self.options["tls_private_key"] = self.options["tls_certificate"]
+
+    def _loadCertificates(self, f):
+        """Read a .pem file with a list of certificates
+
+        @param f (file): file obj (opened .pem file)
+        @return (list[OpenSSL.crypto.X509]): list of certificates
+        @raise OpenSSL.crypto.Error: error while parsing the file
+        """
+        # XXX: didn't found any method to load a .pem file with several certificates
+        #      so the certificates split is done here
+        certificates = []
+        buf = []
+        while True:
+            line = f.readline()
+            buf.append(line)
+            if "-----END CERTIFICATE-----" in line:
+                certificates.append(
+                    OpenSSL.crypto.load_certificate(
+                        OpenSSL.crypto.FILETYPE_PEM, "".join(buf)
+                    )
+                )
+                buf = []
+            elif not line:
+                log.debug(u"{} certificate(s) found".format(len(certificates)))
+                return certificates
+
+    def _loadPKey(self, f):
+        """Read a private key from a .pem file
+
+        @param f (file): file obj (opened .pem file)
+        @return (list[OpenSSL.crypto.PKey]): private key object
+        @raise OpenSSL.crypto.Error: error while parsing the file
+        """
+        return OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, f.read())
+
+    def _loadCertificate(self, f):
+        """Read a public certificate from a .pem file
+
+        @param f (file): file obj (opened .pem file)
+        @return (list[OpenSSL.crypto.X509]): public certificate
+        @raise OpenSSL.crypto.Error: error while parsing the file
+        """
+        return OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, f.read())
+
+    def _getTLSContextFactory(self):
+        """Load TLS certificate and build the context factory needed for listenSSL"""
+        if ssl is None:
+            raise ImportError(u"Python module pyOpenSSL is not installed!")
+
+        cert_options = {}
+
+        for name, option, method in [
+            ("privateKey", "tls_private_key", self._loadPKey),
+            ("certificate", "tls_certificate", self._loadCertificate),
+            ("extraCertChain", "tls_chain", self._loadCertificates),
+        ]:
+            path = self.options[option]
+            if not path:
+                assert option == "tls_chain"
+                continue
+            log.debug(u"loading {option} from {path}".format(option=option, path=path))
+            try:
+                with open(path) as f:
+                    cert_options[name] = method(f)
+            except IOError as e:
+                log.error(
+                    u"Error while reading file {path} for option {option}: {error}".format(
+                        path=path, option=option, error=e
+                    )
+                )
+                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
+                    )
+                )
+                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
+                        )
+                    )
+                self.quit(2)
+
+        return ssl.CertificateOptions(**cert_options)
+
+    ## service management ##
+
+    def _startService(self, dummy=None):
+        """Actually start the HTTP(S) server(s) after the profile for Libervia is connected.
+
+        @raise ImportError: OpenSSL is not available
+        @raise IOError: the certificate file doesn't exist
+        @raise OpenSSL.crypto.Error: the certificate file is invalid
+        """
+        # now that we have service profile connected, we add resource for its cache
+        service_path = regex.pathEscape(C.SERVICE_PROFILE)
+        cache_dir = os.path.join(self.cache_root_dir, u"profiles", service_path)
+        self.cache_resource.putChild(service_path, ProtectedFile(cache_dir))
+        self.service_cache_url = u"/" + os.path.join(C.CACHE_DIR, service_path)
+        session_iface.SATSession.service_cache_url = self.service_cache_url
+
+        if self.options["connection_type"] in ("https", "both"):
+            self._TLSOptionsCheck()
+            context_factory = self._getTLSContextFactory()
+            reactor.listenSSL(self.options["port_https"], self.site, context_factory)
+        if self.options["connection_type"] in ("http", "both"):
+            if (
+                self.options["connection_type"] == "both"
+                and self.options["redirect_to_https"]
+            ):
+                reactor.listenTCP(
+                    self.options["port"],
+                    server.Site(
+                        RedirectToHTTPS(
+                            self.options["port"], self.options["port_https_ext"]
+                        )
+                    ),
+                )
+            else:
+                reactor.listenTCP(self.options["port"], self.site)
+
+    @defer.inlineCallbacks
+    def stopService(self):
+        log.info(_("launching cleaning methods"))
+        for callback, args, kwargs in self._cleanup:
+            callback(*args, **kwargs)
+        try:
+            yield self.bridgeCall("disconnect", C.SERVICE_PROFILE)
+        except Exception:
+            log.warning(u"Can't disconnect service profile")
+
+    def run(self):
+        reactor.run()
+
+    def stop(self):
+        reactor.stop()
+
+    def quit(self, exit_code=None):
+        """Exit app when reactor is running
+
+        @param exit_code(None, int): exit code
+        """
+        self.stop()
+        sys.exit(exit_code or 0)
+
+
+class RedirectToHTTPS(web_resource.Resource):
+    def __init__(self, old_port, new_port):
+        web_resource.Resource.__init__(self)
+        self.isLeaf = True
+        self.old_port = old_port
+        self.new_port = new_port
+
+    def render(self, request):
+        netloc = request.URLPath().netloc.replace(
+            ":%s" % self.old_port, ":%s" % self.new_port
+        )
+        url = "https://" + netloc + request.uri
+        return web_util.redirectTo(url, request)
+
+
+registerAdapter(session_iface.SATSession, server.Session, session_iface.ISATSession)
+registerAdapter(
+    session_iface.SATGuestSession, server.Session, session_iface.ISATGuestSession
+)