Mercurial > libervia-web
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