# HG changeset patch # User Goffi # Date 1447525090 -3600 # Node ID 0df9c62474742888c53ecf514446ec7d699803d3 # Parent ab54af2a9ab2ab39c7857fe61cf86d83767c3d43 core: profile session starting and connection are now separated. Moved profile session starting/authentication to memory module diff -r ab54af2a9ab2 -r 0df9c6247474 src/core/sat_main.py --- a/src/core/sat_main.py Sat Nov 14 19:18:10 2015 +0100 +++ b/src/core/sat_main.py Sat Nov 14 19:18:10 2015 +0100 @@ -18,7 +18,7 @@ # along with this program. If not, see . import sat -from sat.core.i18n import _, D_, languageSwitch +from sat.core.i18n import _, languageSwitch from twisted.application import service from twisted.internet import defer from twisted.words.protocols.jabber import jid @@ -32,7 +32,6 @@ log = getLogger(__name__) from sat.core.constants import Const as C from sat.memory.memory import Memory -from sat.memory.crypto import PasswordHasher from sat.tools import trigger from sat.tools import utils from sat.stdui import ui_contact_list, ui_profile_manager @@ -54,7 +53,7 @@ self._cb_map = {} # map from callback_id to callbacks self._menus = OrderedDict() # dynamic menus. key: callback_id, value: menu data (dictionnary) self.__private_data = {} # used for internal callbacks (key = id) FIXME: to be removed - self._initialised = defer.Deferred() + self.initialised = defer.Deferred() self.profiles = {} self.plugins = {} @@ -66,7 +65,7 @@ except exceptions.BridgeInitError: log.error(u"Bridge can't be initialised, can't start SàT core") sys.exit(1) - self.bridge.register("getReady", lambda: self._initialised) + self.bridge.register("getReady", lambda: self.initialised) self.bridge.register("getVersion", lambda: self.full_version) self.bridge.register("getFeatures", self.getFeatures) self.bridge.register("getProfileName", self.memory.getProfileName) @@ -75,7 +74,7 @@ self.bridge.register("getEntitiesData", self.memory._getEntitiesData) self.bridge.register("asyncCreateProfile", self.memory.asyncCreateProfile) self.bridge.register("asyncDeleteProfile", self.memory.asyncDeleteProfile) - self.bridge.register("asyncConnect", self.asyncConnect) + self.bridge.register("asyncConnect", self._asyncConnect) self.bridge.register("disconnect", self.disconnect) self.bridge.register("getContacts", self.getContacts) self.bridge.register("getContactsFromGroup", self.getContactsFromGroup) @@ -137,7 +136,7 @@ self._import_plugins() ui_contact_list.ContactList(self) ui_profile_manager.ProfileManager(self) - self._initialised.callback(None) + self.initialised.callback(None) log.info(_("Backend is ready")) def _import_plugins(self): @@ -244,39 +243,31 @@ defers_list.append(defer.maybeDeferred(unload)) return defers_list - def asyncConnect(self, profile_key=C.PROF_KEY_NONE, password=''): + def _asyncConnect(self, profile_key, password=''): + profile = self.memory.getProfileName(profile_key) + return self.asyncConnect(profile, password) + + def asyncConnect(self, profile, password=''): """Retrieve the individual parameters, authenticate the profile and initiate the connection to the associated XMPP server. @param password (string): the SàT profile password - @param profile_key: %(doc_profile_key)s - @return: Deferred: - - a deferred boolean if the profile authentication succeed: - - True if the XMPP connection was already established - - False if the XMPP connection has been initiated (it may still fail) - - a Failure if the profile authentication failed + @param profile: %(doc_profile)s + @return (D(bool)): + - True if the XMPP connection was already established + - False if the XMPP connection has been initiated (it may still fail) + @raise exceptions.PasswordError: Profile password is wrong """ - def backendInitialised(dummy): - profile = self.memory.getProfileName(profile_key) - if not profile: - log.error(_('Trying to connect a non-existant profile')) - raise exceptions.ProfileUnknownError(profile_key) + def connectXMPPClient(dummy=None): + if self.isConnected(profile): + log.info(_("already connected !")) + return True + d = self._connectXMPPClient(profile) + return d.addCallback(lambda dummy: False) - def connectXMPPClient(dummy=None): - if self.isConnected(profile): - log.info(_("already connected !")) - return True - self.memory.startProfileSession(profile) - d = self.memory.loadIndividualParams(profile) - d.addCallback(lambda dummy: self._connectXMPPClient(profile)) - return d.addCallback(lambda dummy: False) - - d = self._authenticateProfile(password, profile) - d.addCallback(connectXMPPClient) - return d - - self._initialised.addErrback(lambda dummy: None) # allow successive attempts - return self._initialised.addCallback(backendInitialised) + d = self.memory.startSession(password, profile) + d.addCallback(connectXMPPClient) + return d @defer.inlineCallbacks def _connectXMPPClient(self, profile): @@ -348,31 +339,6 @@ yield list_d.addCallback(logPluginResults) # FIXME: we should have a timeout here, and a way to know if a plugin freeze # TODO: mesure launch time of each plugin - def _authenticateProfile(self, password, profile): - """Authenticate the profile. - - @param password (string): the SàT profile password - @param profile: %(doc_profile)s - @return: Deferred: a deferred None in case of success, a failure otherwise. - """ - session_data = self.memory.auth_sessions.profileGetUnique(profile) - if not password and session_data: - # XXX: this allows any frontend to connect with the empty password as soon as - # the profile has been authenticated at least once before. It is OK as long as - # submitting a form with empty passwords is restricted to local frontends. - return defer.succeed(None) - - def check_result(result): - if not result: - log.warning(_(u'Authentication failure of profile %s') % profile) - raise exceptions.PasswordError(D_("The provided profile password doesn't match.")) - if not session_data: # avoid to create two profile sessions when password if specified - return self.memory.newAuthSession(password, profile) - - d = self.memory.asyncGetParamA(C.PROFILE_PASS_PATH[1], C.PROFILE_PASS_PATH[0], profile_key=profile) - d.addCallback(lambda sat_cipher: PasswordHasher.verify(password, sat_cipher)) - return d.addCallback(check_result) - def disconnect(self, profile_key): """disconnect from jabber server""" if not self.isConnected(profile_key): diff -r ab54af2a9ab2 -r 0df9c6247474 src/memory/memory.py --- a/src/memory/memory.py Sat Nov 14 19:18:10 2015 +0100 +++ b/src/memory/memory.py Sat Nov 14 19:18:10 2015 +0100 @@ -37,6 +37,7 @@ from sat.memory.params import Params from sat.memory.disco import Discovery from sat.memory.crypto import BlockCipher +from sat.memory.crypto import PasswordHasher from sat.tools import config as tools_config @@ -316,11 +317,80 @@ ## Profiles/Sessions management ## - def startProfileSession(self, profile): + def _startSession(self, password, profile_key): + profile = self.getProfileName(profile_key) + return self.startSession(password, profile) + + def startSession(self, password, profile): """"Iniatialise session for a profile - @param profile: %(doc_profile)s""" - log.info(_("[%s] Profile session started" % profile)) - self._entities_cache[profile] = {} + + @param password(unicode): profile session password + or empty string is no password is set + @param profile: %(doc_profile)s + """ + def createSession(dummy): + """Called once params are loaded.""" + self._entities_cache[profile] = {} + log.info(u"[{}] Profile session started".format(profile)) + return False + + def backendInitialised(dummy): + def doStartSession(dummy=None): + if self.isSessionStarted(profile): + log.info("Session already started!") + return True + try: + # if there is a value at this point in self._entities_cache, + # it is the loadIndividualParams Deferred, the session is starting + session_d = self._entities_cache[profile] + except KeyError: + # else we do request the params + session_d = self._entities_cache[profile] = self.loadIndividualParams(profile) + session_d.addCallback(createSession) + finally: + return session_d + + auth_d = self.profileAuthenticate(password, profile) + auth_d.addCallback(doStartSession) + return auth_d + + return self.host.initialised.addCallback(backendInitialised) + + def _isSessionStarted(self, profile_key): + return self.isSessionStarted(self.getProfileName(profile_key)) + + def isSessionStarted(self, profile): + try: + # XXX: if the value in self._entities_cache is a Deferred, + # the session is starting but not started yet + return not isinstance(self._entities_cache[profile], defer.Deferred) + except KeyError: + return False + + def profileAuthenticate(self, password, profile): + """Authenticate the profile. + + @param password (unicode): the SàT profile password + @param profile: %(doc_profile)s + @return (D): a deferred None in case of success, a failure otherwise. + """ + session_data = self.auth_sessions.profileGetUnique(profile) + if not password and session_data: + # XXX: this allows any frontend to connect with the empty password as soon as + # the profile has been authenticated at least once before. It is OK as long as + # submitting a form with empty passwords is restricted to local frontends. + return defer.succeed(None) + + def check_result(result): + if not result: + log.warning(u'Authentication failure of profile {}'.format(profile)) + raise exceptions.PasswordError(u"The provided profile password doesn't match.") + if not session_data: # avoid to create two profile sessions when password if specified + return self.newAuthSession(password, profile) + + d = self.asyncGetParamA(C.PROFILE_PASS_PATH[1], C.PROFILE_PASS_PATH[0], profile_key=profile) + d.addCallback(lambda sat_cipher: PasswordHasher.verify(password, sat_cipher)) + return d.addCallback(check_result) def newAuthSession(self, key, profile): """Start a new session for the authenticated profile.