changeset 961:22fe06569b1a

server: moved logging worflow in separated method, so it can be used by Libervia Pages
author Goffi <goffi@goffi.org>
date Fri, 27 Oct 2017 18:35:23 +0200
parents e59edcae4c18
children c7fba7709d05
files src/browser/sat_browser/register.py src/common/constants.py src/server/server.py
diffstat 3 files changed, 275 insertions(+), 168 deletions(-) [+]
line wrap: on
line diff
--- a/src/browser/sat_browser/register.py	Fri Oct 27 18:31:42 2017 +0200
+++ b/src/browser/sat_browser/register.py	Fri Oct 27 18:35:23 2017 +0200
@@ -210,7 +210,7 @@
         elif result == C.XMPP_AUTH_ERROR:
             # TODO: call stdui action CHANGE_XMPP_PASSWD_ID as it's done in primitivus
             Window.alert(_(u'Your XMPP account failed to connect. Did you enter the good password? If you have changed your XMPP password since your last connection on Libervia, please use another SàT frontend to update your profile.'))
-        elif result == C.PROFILE_LOGGED_REGISTERED_WITH_EXT_JID:
+        elif result == C.PROFILE_LOGGED_EXT_JID:
             self.callback()
             Window.alert(_('A profile has been created on this Libervia service using your existing XMPP account. Since you are not using our XMPP server, we can not guaranty that all the extra features (blog, directory...) will fully work.'))
         elif result == C.PROFILE_LOGGED:
--- a/src/common/constants.py	Fri Oct 27 18:31:42 2017 +0200
+++ b/src/common/constants.py	Fri Oct 27 18:35:23 2017 +0200
@@ -28,19 +28,25 @@
     APP_VERSION = u'0.7.0D'  # Please add 'D' at the end for dev versions
     LIBERVIA_MAIN_PAGE = "libervia.html"
 
-    # MISC
-    PASSWORD_MIN_LENGTH = 6  # for new account creation
+    # REGISTRATION
+    # XXX: for now libervia forces the creation to lower case
+    # XXX: Regex patterns must be compatible with both Python and JS
+    REG_LOGIN_RE = r'^[a-z0-9_-]+$'
+    REG_EMAIL_RE = r'^.+@.+\..+'
+    PASSWORD_MIN_LENGTH = 6
 
     # HTTP REQUEST RESULT VALUES
     PROFILE_AUTH_ERROR = 'PROFILE AUTH ERROR'
     XMPP_AUTH_ERROR = 'XMPP AUTH ERROR'
     ALREADY_WAITING = 'ALREADY WAITING'
     SESSION_ACTIVE = 'SESSION ACTIVE'
+    NOT_CONNECTED = 'NOT CONNECTED'
     PROFILE_LOGGED = 'LOGGED'
-    PROFILE_LOGGED_REGISTERED_WITH_EXT_JID = 'LOGGED (REGISTERED WITH EXTERNAL JID)'
+    PROFILE_LOGGED_EXT_JID = 'LOGGED (REGISTERED WITH EXTERNAL JID)'
     ALREADY_EXISTS = 'ALREADY EXISTS'
     REGISTRATION_SUCCEED = 'REGISTRATION'
     INTERNAL_ERROR = 'INTERNAL ERROR'
+    INVALID_INPUT = 'INVALID INPUT'
     BAD_REQUEST = 'BAD REQUEST'
     NO_REPLY = 'NO REPLY'
     NOT_ALLOWED = 'NOT ALLOWED'
--- a/src/server/server.py	Fri Oct 27 18:31:42 2017 +0200
+++ b/src/server/server.py	Fri Oct 27 18:35:23 2017 +0200
@@ -889,7 +889,6 @@
         JSONRPCMethodManager.__init__(self, sat_host)
         self.profiles_waiting = {}
         self.request = None
-        self.waiting_profiles = WaitingRequests()
 
     def render(self, request):
         """
@@ -927,28 +926,42 @@
             return C.BAD_REQUEST
 
         if submit_type == 'register':
-            if not self.sat_host.options["allow_registration"]:
-                log.warning(u"Registration received while it is not allowed, hack attempt?")
-                return exceptions.PermissionError(u"Registration is not allowed on this server")
-            return self._registerNewAccount(request)
+            self._registerNewAccount(request)
+            return server.NOT_DONE_YET
         elif submit_type == 'login':
-            d = self.asyncBridgeCall("getNewAccountDomain")
-            d.addCallback(lambda domain: self._loginAccount(request, domain))
+            self._loginAccount(request)
             return server.NOT_DONE_YET
         return Exception('Unknown submit type')
 
-    def _loginAccount(self, request, new_account_domain):
+    @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.
 
-        @param request: request of the register form
-        @param new_account_domain (unicode): host corresponding to the local domain
-        @return: a constant indicating the state:
+        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 profile
-            - server.NOT_DONE_YET: the profile is being processed, the return
-                value will be given by self._logged or auth_eb
+            - 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]
@@ -960,161 +973,40 @@
 
         assert login
 
-        if login.startswith('@'):  # this is checked by javascript but also here for security reason
-            # FIXME: return an error instead of an Exception?
-            raise Exception('No profile_key allowed')
-
-        if '@' in login:
-            try:
-                login_jid = jid.JID(login)
-            except (RuntimeError, jid.InvalidFormat, AttributeError):
-                request.write(C.PROFILE_AUTH_ERROR)
-                request.finish()
-                return
-
-            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 = self.sat_host.bridge.profileNameGet(login)
-        except Exception:  # XXX: ProfileUnknownError wouldn't work, it's encapsulated
-            if login_jid is not None and login_jid.user:  # try to create a new sat profile using the XMPP credentials
-                if not self.sat_host.options["allow_registration"]:
-                    log.warning(u"Trying to register JID account while registration is not allowed")
-                    request.write(C.PROFILE_AUTH_ERROR)
-                    request.finish()
-                    return
-                profile = login # FIXME: what if there is a resource?
-                connect_method = "asyncConnectWithXMPPCredentials"
-                register_with_ext_jid = True
-            else: # non existing username
-                request.write(C.PROFILE_AUTH_ERROR)
-                request.finish()
-                return
-        else:
-            if profile != login or (not password and profile not in self.sat_host.options['empty_password_allowed_warning_dangerous_list']):
-                # profiles with empty passwords are restricted to local frontends
-                request.write(C.PROFILE_AUTH_ERROR)
-                request.finish()
-                return
-            register_with_ext_jid = False
-
-            connect_method = "connect"
-
-        if self.waiting_profiles.getRequest(profile):
+            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
-
-        def auth_eb(failure):
-            fault = failure.value.faultString
-            self.waiting_profiles.purgeRequest(profile)
-            if fault in ('PasswordError', 'ProfileUnknownError'):
-                log.info(u"Profile %s doesn't exist or the submitted password is wrong" % profile)
-                request.write(C.PROFILE_AUTH_ERROR)
-            elif fault == 'SASLAuthError':
-                log.info(u"The XMPP password of profile %s is wrong" % profile)
-                request.write(C.XMPP_AUTH_ERROR)
-            elif fault == 'NoReply':
-                log.info(_("Did not receive a reply (the timeout expired or the connection is broken)"))
-                request.write(C.NO_REPLY)
-            else:
-                log.error(u'Unmanaged fault string "%s" in errback for the connection of profile %s' % (fault, profile))
-                request.write(fault)
+        except exceptions.TimeOutError:
+            request.write(C.NO_REPLY)
             request.finish()
-
-        self.waiting_profiles.setRequest(request, profile, register_with_ext_jid)
-        d = self.asyncBridgeCall(connect_method, profile, password)
-        d.addCallbacks(lambda connected: self._logged(profile, request) if connected else None, auth_eb)
-
-    def _registerNewAccount(self, request):
-        """Create a new account, or return error
-        @param request: request of the register form
-        @return: a constant indicating the state:
-            - C.BAD_REQUEST: something is wrong in the request (bad arguments)
-            - 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
-            - server.NOT_DONE_YET: the profile is being processed, the return
-                value will be given later (one of those previously described)
-        """
-        try:
-            # XXX: for now libervia forces the creation to lower case
-            profile = login = request.args['register_login'][0].lower()
-            password = request.args['register_password'][0]
-            email = request.args['email'][0]
-        except KeyError:
-            return C.BAD_REQUEST
-        if not re.match(r'^[a-z0-9_-]+$', login, re.IGNORECASE) or \
-           not re.match(r'^.+@.+\..+', email, re.IGNORECASE) or \
-           len(password) < C.PASSWORD_MIN_LENGTH:
-            return C.BAD_REQUEST
-
-        def registered(result):
-            request.write(C.REGISTRATION_SUCCEED)
+            return
+        except exceptions.InternalError as e:
+            request.write(e.message)
             request.finish()
-
-        def registeringError(failure):
-            reason = failure.value.faultString
-            if reason == "ConflictError":
-                request.write(C.ALREADY_EXISTS)
-            elif reason == "InternalError":
-                request.write(C.INTERNAL_ERROR)
-            else:
-                log.error(u'Unknown registering error: %s' % (reason,))
-                request.write(reason)
-            request.finish()
-
-        d = self.asyncBridgeCall("registerSatAccount", email, password, profile)
-        d.addCallback(registered)
-        d.addErrback(registeringError)
-        return server.NOT_DONE_YET
-
-    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.SESSION_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!'))
+            return
+        except exceptions.ConflictError:
             request.write(C.SESSION_ACTIVE)
             request.finish()
             return
-        # we manage profile server side to avoid profile spoofing
-        sat_session.profile = profile
-        self.sat_host.prof_connected.add(profile)
-        cache_dir = os.path.join(self.sat_host.cache_root_dir, regex.pathEscape(profile))
-        # FIXME: would be better to have a global /cache URL which redirect to profile's cache directory, without uuid
-        self.sat_host.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))
+        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
 
-        def onExpire():
-            log.info(u"Session expired (profile=%s)" % (profile,))
-            self.sat_host.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.sat_host.signal_handler.queue[profile]
-            except KeyError:
-                pass
-            #and now we disconnect the profile
-            self.sat_host.bridge.disconnect(profile)
-
-        _session.notifyOnExpire(onExpire)
-
-        request.write(C.PROFILE_LOGGED_REGISTERED_WITH_EXT_JID if register_with_ext_jid else C.PROFILE_LOGGED)
+        assert status
+        request.write(status)
         request.finish()
 
     def jsonrpc_isConnected(self):
@@ -1215,7 +1107,12 @@
                 source_defer = self.signalDeferred[profile]
                 if source_defer.called and source_defer.result[0] == "disconnected":
                     log.info(u"[%s] disconnected" % (profile,))
-                    _session.expire()
+                    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")
 
@@ -1272,11 +1169,13 @@
         @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.register.waiting_profiles.getRequest(profile)
-        if request:
-            self.register._logged(profile, request)
+        # 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 not profile in self.sat_host.prof_connected:
@@ -1874,6 +1773,7 @@
     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')
@@ -2022,8 +1922,209 @@
         getattr(self.bridge, method_name)(*args, **kwargs)
         return d
 
+    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, 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.bridge.disconnect(profile)
+
+        _session.notifyOnExpire(onExpire)
+
+        return C.PROFILE_LOGGED_EXT_JID if register_with_ext_jid else C.PROFILE_LOGGED
+
+    @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 '@' in 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
+        defer.returnValue(self._logged(profile, request))
+
+    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