diff src/server/server.py @ 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 968eda9e982a
children c7fba7709d05
line wrap: on
line diff
--- 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