diff src/browser/sat_browser/plugin_sec_otr.py @ 663:423182fea41c frontends_multi_profiles

browser_side: fixes OTR using the new resource system and proper triggers (send and receive message) or listener (presence update)
author souliane <souliane@mailoo.org>
date Tue, 03 Mar 2015 07:21:50 +0100
parents ebb602d8b3f2
children 8449a5db0602
line wrap: on
line diff
--- a/src/browser/sat_browser/plugin_sec_otr.py	Tue Mar 03 06:51:13 2015 +0100
+++ b/src/browser/sat_browser/plugin_sec_otr.py	Tue Mar 03 07:21:50 2015 +0100
@@ -23,10 +23,12 @@
 The text messages to display are mostly taken from the Pidgin OTR plugin (GPL 2.0, see http://otr.cypherpunks.ca).
 """
 
+from sat.core.log import getLogger
+log = getLogger(__name__)
+
 from sat.core.i18n import _, D_
-from sat.core.log import getLogger
 from sat.core import exceptions
-log = getLogger(__name__)
+from sat.tools.misc import TriggerManager
 
 from constants import Const as C
 from sat_frontends.tools import jid
@@ -39,7 +41,6 @@
 PRIVATE_KEY = "PRIVATE KEY"
 MAIN_MENU = D_('OTR encryption')
 DIALOG_EOL = "<br />"
-DIALOG_USERS_ML = D_("<a href='mailto:users@salut-a-toi.org?subject={subject}&body=Please give us some hints about how to reproduce the bug (your browser name and version, what you did and what happened)'>users@salut-a-toi.org</a>")
 
 AUTH_TRUSTED = D_("Verified")
 AUTH_UNTRUSTED = D_("Unverified")
@@ -72,7 +73,8 @@
 AKE_ENCRYPTED = D_(" conversation with {jid} started. Your client is not logging this conversation.")
 AKE_NOT_ENCRYPTED = D_("ERROR: successfully ake'd with {jid} but the conversation is not encrypted!")
 END_ENCRYPTED = D_("ERROR: the OTR session ended but the context is still supposedly encrypted!")
-END_PLAIN = D_("Your conversation with {jid} is no more or hasn't been encrypted.")
+END_PLAIN_NO_MORE = D_("Your conversation with {jid} is no more encrypted.")
+END_PLAIN_HAS_NOT = D_("Your conversation with {jid} hasn't been encrypted.")
 END_FINISHED = D_("{jid} has ended his or her private conversation with you; you should do the same.")
 
 KEY_TITLE = D_('Private key')
@@ -92,15 +94,13 @@
 
 ACTION_NA_TITLE = D_("Impossible action")
 ACTION_NA = D_("Your correspondent must be connected to start an OTR conversation with him.")
-RESOURCE_ISSUE_TITLE = D_("Security issue")
-RESOURCE_ISSUE = D_("Your correspondent's resource is unknown!{eol}{eol}You should stop any OTR conversation with {jid} to avoid sending him unencrypted messages in an encrypted context.{eol}{eol}Please report the bug to the users mailing list: {users_ml}.")
 
 DEFAULT_POLICY_FLAGS = {
     'ALLOW_V2': True,
     'ALLOW_V3': True,
     'REQUIRE_ENCRYPTION': False,
-    'SEND_WHITESPACE_TAG': False,  # FIXME: we need to complete sendMessageTrigger before turning this to True
-    'WHITESPACE_START_AKE': False,  # FIXME: we need to complete messageReceivedTrigger before turning this to True
+    'SEND_WHITESPACE_TAG': False,  # FIXME: we need to complete sendMessageTg before turning this to True
+    'WHITESPACE_START_AKE': False,  # FIXME: we need to complete newMessageTg before turning this to True
 }
 
 # list a couple of texts or htmls (untrusted, trusted) for each state
@@ -120,6 +120,13 @@
 }
 
 
+unicode = str  # FIXME: pyjamas workaround
+
+
+class NotConnectedEntity(Exception):
+    pass
+
+
 class Context(otr.context.Context):
 
     def __init__(self, host, account, other_jid):
@@ -158,13 +165,13 @@
         if not encrypted:
             if self.state == otr.context.STATE_ENCRYPTED:
                 log.warning(u"Received unencrypted message in an encrypted context (from %(jid)s)" % {'jid': self.peer})
-                self.host.newMessageCb(self.peer, RECEIVE_PLAIN_IN_ENCRYPTED_CONTEXT, C.MESS_TYPE_INFO, self.host.whoami, {})
-        self.host.newMessageCb(self.peer, msg, "chat", self.host.whoami, {})
+                self.host.newMessageHandler(unicode(self.peer), RECEIVE_PLAIN_IN_ENCRYPTED_CONTEXT, C.MESS_TYPE_INFO, unicode(self.host.whoami), {})
+        self.host.newMessageHandler(unicode(self.peer), msg, C.MESS_TYPE_CHAT, unicode(self.host.whoami), {})
 
     def sendMessageCb(self, msg, meta=None):
         assert isinstance(self.peer, jid.JID)
         log.debug("message to send%s: %s" % ((' (attached meta data: %s)' % meta) if meta else '', msg))
-        self.host.bridge.call('sendMessage', (None, self.host.sendError), self.peer, msg, '', 'chat', {'send_only': 'true'})
+        self.host.bridge.call('sendMessage', (None, self.host.sendError), unicode(self.peer), msg, '', C.MESS_TYPE_CHAT, {'send_only': 'true'})
 
     def messageErrorCb(self, error):
         log.error('error occured: %s' % error)
@@ -186,13 +193,13 @@
 
         elif status == otr.context.STATUS_END_OTR:
             if msg_state == otr.context.STATE_PLAINTEXT:
-                feedback = END_PLAIN
+                feedback = END_PLAIN_NO_MORE
             elif msg_state == otr.context.STATE_ENCRYPTED:
                 log.error(END_ENCRYPTED)
             elif msg_state == otr.context.STATE_FINISHED:
                 feedback = END_FINISHED
 
-        self.host.newMessageCb(self.peer, feedback.format(jid=other_jid_s), C.MESS_TYPE_INFO, self.host.whoami, {'header_info': OTR.getInfoText(msg_state, trust)})
+        self.host.newMessageHandler(unicode(self.peer), feedback.format(jid=other_jid_s), C.MESS_TYPE_INFO, unicode(self.host.whoami), {'header_info': OTR.getInfoText(msg_state, trust)})
 
     def setCurrentTrust(self, new_trust='', act='asked', type_='trust'):
         log.debug("setCurrentTrust: trust={trust}, act={act}, type={type}".format(type=type_, trust=new_trust, act=act))
@@ -215,7 +222,7 @@
         otr.context.Context.setCurrentTrust(self, new_trust)
         if old_trust != new_trust:
             feedback = AUTH_STATUS.format(state=(AUTH_TRUSTED if new_trust else AUTH_UNTRUSTED).lower())
-            self.host.newMessageCb(self.peer, feedback, C.MESS_TYPE_INFO, self.host.whoami, {'header_info': OTR.getInfoText(self.state, new_trust)})
+            self.host.newMessageHandler(unicode(self.peer), feedback, C.MESS_TYPE_INFO, unicode(self.host.whoami), {'header_info': OTR.getInfoText(self.state, new_trust)})
 
     def fingerprintAuthCb(self):
         """OTR v2 authentication using manual fingerprint comparison"""
@@ -327,7 +334,7 @@
         self.contexts = {}
 
     def startContext(self, other_jid):
-        assert isinstance(other_jid, jid.JID)
+        assert isinstance(other_jid, jid.JID)  # never start an OTR session with a bare JID
         # FIXME upstream: apparently pyjamas doesn't implement setdefault well, it ignores JID.__hash__ redefinition
         #context = self.contexts.setdefault(other_jid, Context(self.host, self.account, other_jid))
         if other_jid not in self.contexts:
@@ -341,20 +348,30 @@
         @param start (bool): start non-existing context if True
         @return: Context
         """
+        try:
+            other_jid = self.fixResource(other_jid)
+        except NotConnectedEntity:
+            log.debug(u"getContextForUser [%s]: not connected!" % other_jid)
+            return None
         log.debug(u"getContextForUser [%s]" % other_jid)
-        if not other_jid.resource:
-            log.error("getContextForUser called with a bare jid")
-            running_sessions = [jid_.bare for jid_ in self.contexts.keys() if self.contexts[jid_].state == otr.context.STATE_ENCRYPTED]
-            if start or (other_jid in running_sessions):
-                users_ml = DIALOG_USERS_ML.format(subject=D_("OTR issue in Libervia: getContextForUser called with a bare jid in an encrypted context"))
-                text = RESOURCE_ISSUE.format(eol=DIALOG_EOL, jid=other_jid, users_ml=users_ml)
-                dialog.InfoDialog(RESOURCE_ISSUE_TITLE, text, AddStyleName="maxWidthLimit").show()
-            return None  # never start an OTR session with a bare JID
         if start:
             return self.startContext(other_jid)
         else:
             return self.contexts.get(other_jid, None)
 
+    def fixResource(self, other_jid):
+        """Return the full JID in case the resource of the given JID is missing.
+
+        @param other_jid (jid.JID): JID to check
+        @return jid.JID
+        """
+        if other_jid.resource:
+            return other_jid
+        clist = self.host.contact_list
+        if clist.getCache(other_jid.bare, C.PRESENCE_SHOW) is None:
+            raise NotConnectedEntity
+        return clist.getFullJid(other_jid)
+
 
 class OTR(object):
 
@@ -362,8 +379,9 @@
         log.info(_(u"OTR plugin initialization"))
         self.host = host
         self.context_manager = None
-        self.last_resources = {}
         self.host.bridge._registerMethods(["skipOTR"])
+        self.host.trigger.add("newMessage", self.newMessageTg, priority=TriggerManager.MAX_PRIORITY)
+        self.host.trigger.add("sendMessage", self.sendMessageTg, priority=TriggerManager.MAX_PRIORITY)
 
     @classmethod
     def getInfoText(self, state=otr.context.STATE_PLAINTEXT, trust=''):
@@ -377,20 +395,16 @@
             state = OTR_MSG_STATES.keys()[0]
         return OTR_MSG_STATES[state][1 if trust else 0]
 
-    def infoTextCallback(self, other_jid, cb):
-        """Get the current info text for a conversation and run a callback.
+    def getInfoTextForUser(self, other_jid):
+        """Get the current info text for a conversation.
 
         @param other_jid (jid.JID): JID of the correspondant
-        @paam cb (callable): method to be called with the computed info text
         """
-        def gotResource(other_jid):
-            otrctx = self.context_manager.getContextForUser(other_jid, start=False)
-            if otrctx is None:
-                cb(OTR.getInfoText())
-            else:
-                cb(OTR.getInfoText(otrctx.state, otrctx.getCurrentTrust()))
-
-        self.fixResource(other_jid, gotResource)
+        otrctx = self.context_manager.getContextForUser(other_jid, start=False)
+        if otrctx is None:
+            return OTR.getInfoText()
+        else:
+            return OTR.getInfoText(otrctx.state, otrctx.getCurrentTrust())
 
     def inhibitMenus(self):
         """Tell the caller which dynamic menus should be inhibited"""
@@ -410,112 +424,80 @@
         # decrypt it, parse it with otr.crypt.PK.parsePrivateKey(privkey) and
         # assign it to self.context_manager.account.privkey
 
+        # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword)
+        self.presenceListener = self.onPresenceUpdate
+        self.host.addListener('presence', self.presenceListener, [C.PROF_KEY_NONE])
+
     def profileDisconnected(self):
         for context in self.context_manager.contexts.values():
             context.disconnect()
+        self.host.removeListener('presence', self.presenceListener)
 
-    def fixResource(self, jid_, cb):
-        # FIXME: it's dirty, but libervia doesn't manage resources correctly now, refactoring is planed
-        if jid_.resource:
-            self.last_resources[jid_.bare] = jid_.resource
-            cb(jid_)
-        elif jid_.bare in self.last_resources:
-            # FIXME: to be removed: must use new resource system
-            # jid_.setResource(self.last_resources[jid_.bare])
-            cb(jid_)
-        else:
-            pass # FIXME: to be removed: must use new resource system
-            # def gotResource(resource):
-            #     if resource:
-            #         jid_.setResource(resource)
-            #         self.last_resources[jid_.bare] = jid_.resource
-            #     cb(jid_)
-            #
-            # self.host.bridge.call('getLastResource', gotResource, jid_)
-
-    def messageReceivedTrigger(self, from_jid, msg, msg_type, to_jid, extra):
-        if msg_type == C.MESS_TYPE_INFO:
+    def newMessageTg(self, from_jid, msg, msg_type, to_jid, extra, profile):
+        if msg_type != C.MESS_TYPE_CHAT:
             return True
 
         tag = otr.proto.checkForOTR(msg)
         if tag is None or (tag == otr.context.WHITESPACE_TAG and not DEFAULT_POLICY_FLAGS['WHITESPACE_START_AKE']):
             return True
 
-        def decrypt(context):
-            context.receiveMessage(msg)
+        other_jid = to_jid if from_jid.bare == self.host.whoami.bare else from_jid
+        otrctx = self.context_manager.getContextForUser(other_jid, start=False)
+        if otrctx is None:
+            def confirm(confirm):
+                if confirm:
+                    self.host.displayWidget(chat.Chat, other_jid)
+                    self.context_manager.startContext(other_jid).receiveMessage(msg)
+                else:
+                    # FIXME: plain text messages with whitespaces would be lost here when WHITESPACE_START_AKE is True
+                    pass
+            key = self.context_manager.account.privkey
+            question = QUERY_RECEIVED + QUERY_SLOWDOWN + (QUERY_KEY if key else QUERY_NO_KEY) + QUERY_CONFIRM
+            dialog.ConfirmDialog(confirm, question.format(jid=other_jid, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show()
+        else:  # do not ask for user confirmation if the context exist
+            otrctx.receiveMessage(msg)
 
-        def cb(jid_):
-            otrctx = self.context_manager.getContextForUser(jid_, start=False)
-            if otrctx is None:
-                def confirm(confirm):
-                    if confirm:
-                        self.host.displayWidget(chat.Chat, jid_)
-                        decrypt(self.context_manager.startContext(jid_))
-                    else:
-                        # FIXME: plain text messages with whitespaces would be lost here when WHITESPACE_START_AKE is True
-                        pass
-                key = self.context_manager.account.privkey
-                msg = QUERY_RECEIVED + QUERY_SLOWDOWN + (QUERY_KEY if key else QUERY_NO_KEY) + QUERY_CONFIRM
-                dialog.ConfirmDialog(confirm, msg.format(jid=jid_, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show()
-            else:  # do not ask if the context exist
-                decrypt(otrctx)
-
-        other_jid = to_jid if from_jid.bare == self.host.whoami.bare else from_jid
-        self.fixResource(other_jid, cb)
         return False  # interrupt the main process
 
-    def sendMessageTrigger(self, to_jid, msg, msg_type, extra):
-        def cb(jid_):
-            otrctx = self.context_manager.getContextForUser(jid_, start=False)
-            if otrctx is not None and otrctx.state != otr.context.STATE_PLAINTEXT:
-                if otrctx.state == otr.context.STATE_ENCRYPTED:
-                    log.debug(u"encrypting message")
-                    otrctx.sendMessage(msg)
-                    self.host.newMessageCb(self.host.whoami, msg, msg_type, jid_, extra)
-                else:
-                    feedback = SEND_PLAIN_IN_FINISHED_CONTEXT
-                    dialog.InfoDialog(FINISHED_CONTEXT_TITLE.format(jid=to_jid), feedback, AddStyleName="maxWidthLimit").show()
+    def sendMessageTg(self, to_jid, message, subject, mess_type, extra, callback, errback, profile_key):
+        if mess_type != C.MESS_TYPE_CHAT:
+            return True
+
+        otrctx = self.context_manager.getContextForUser(to_jid, start=False)
+        if otrctx is not None and otrctx.state != otr.context.STATE_PLAINTEXT:
+            if otrctx.state == otr.context.STATE_ENCRYPTED:
+                log.debug(u"encrypting message")
+                otrctx.sendMessage(message)
+                self.host.newMessageHandler(unicode(self.host.whoami), message, mess_type, unicode(to_jid), extra)
             else:
-                log.debug(u"sending message unencrypted")
-                self.host.bridge.call('sendMessage', (None, self.host.sendError), to_jid, msg, '', msg_type, extra)
+                feedback = SEND_PLAIN_IN_FINISHED_CONTEXT
+                dialog.InfoDialog(FINISHED_CONTEXT_TITLE.format(jid=to_jid), feedback, AddStyleName="maxWidthLimit").show()
+            return False  # interrupt the main process
 
-        if msg_type == 'groupchat':
-            return True
-        self.fixResource(to_jid, cb)
-        return False  # interrupt the main process
+        log.debug(u"sending message unencrypted")
+        return True
 
-    def presenceReceivedTrigger(self, entity, show, priority, statuses):
+    def onPresenceUpdate(self, entity, show, priority, statuses, profile):
         if show == C.PRESENCE_UNAVAILABLE:
             self.endSession(entity, finish=True)
-        return True
 
-    def endSession(self, other_jid, profile, finish=False):
+    def endSession(self, other_jid, finish=False):
         """Finish or disconnect an OTR session
 
-        @param other_jid (jid.JID): contact JID
+        @param other_jid (jid.JID): other JID
         @param finish: if True, finish the session but do not disconnect it
         @return: True if the session has been finished or disconnected, False if there was nothing to do
         """
-        def cb(other_jid):
-            def not_available():
-                if not finish:
-                    self.host.newMessageCb(other_jid, END_PLAIN.format(jid=other_jid), C.MESS_TYPE_INFO, self.host.whoami, {})
-
-            priv_key = self.context_manager.account.privkey
-            if priv_key is None:
-                not_available()
-                return
-
-            otrctx = self.context_manager.getContextForUser(other_jid, start=False)
-            if otrctx is None:
-                not_available()
-                return
-            if finish:
-                otrctx.finish()
-            else:
-                otrctx.disconnect()
-
-        self.fixResource(other_jid, cb)
+        # checking for private key existence is not needed, context checking is enough
+        otrctx = self.context_manager.getContextForUser(other_jid, start=False)
+        if otrctx is None or otrctx.state == otr.context.STATE_PLAINTEXT:
+            if not finish:
+                self.host.newMessageHandler(unicode(other_jid), END_PLAIN_HAS_NOT.format(jid=other_jid), C.MESS_TYPE_INFO, unicode(self.host.whoami), {})
+            return
+        if finish:
+            otrctx.finish()
+        else:
+            otrctx.disconnect()
 
     # Menu callbacks
 
@@ -529,38 +511,28 @@
             if otrctx:
                 otrctx.sendQueryMessage()
 
-        def cb(jid_):
-            key = self.context_manager.account.privkey
-            if key is None:
-                def confirm(confirm):
-                    if confirm:
-                        query(jid_)
-                msg = QUERY_SEND + QUERY_SLOWDOWN + QUERY_NO_KEY + QUERY_CONFIRM
-                dialog.ConfirmDialog(confirm, msg.format(jid=jid_, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show()
-            else:  # on query reception we ask always, if we initiate we just ask the first time
-                query(jid_)
+        other_jid = menu_data['jid']
+        clist = self.host.contact_list
+        if clist.getCache(other_jid.bare, C.PRESENCE_SHOW) is None:
+            dialog.InfoDialog(ACTION_NA_TITLE, ACTION_NA, AddStyleName="maxWidthLimit").show()
+            return
 
-        try:
-            other_jid = menu_data['jid']
-            contact_list = self.host.contact_list
-            if contact_list.getCache(other_jid.bare, C.PRESENCE_SHOW) is None:
-                dialog.InfoDialog(ACTION_NA_TITLE, ACTION_NA, AddStyleName="maxWidthLimit").show()
-                return
-            self.fixResource(other_jid, cb)
-        except KeyError:
-            log.error(_("jid key is not present !"))
+        key = self.context_manager.account.privkey
+        if key is None:
+            def confirm(confirm):
+                if confirm:
+                    query(other_jid)
+            msg = QUERY_SEND + QUERY_SLOWDOWN + QUERY_NO_KEY + QUERY_CONFIRM
+            dialog.ConfirmDialog(confirm, msg.format(jid=other_jid, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show()
+        else:  # on query reception we ask always, if we initiate we just ask the first time
+            query(other_jid)
 
     def _endSession(self, menu_data):
         """End an OTR session
 
         @param menu_data: %(menu_data)s
         """
-        try:
-            other_jid = menu_data['jid']
-        except KeyError:
-            log.error(_("jid key is not present !"))
-            return None
-        self.endSession(other_jid)
+        self.endSession(menu_data['jid'])
 
     def _authenticate(self, menu_data, profile):
         """Authenticate other user and see our own fingerprint
@@ -571,30 +543,20 @@
         def not_available():
             dialog.InfoDialog(AUTH_TRUST_NA_TITLE, AUTH_TRUST_NA_TXT, AddStyleName="maxWidthLimit").show()
 
-        priv_key = self.context_manager.account.privkey
-        if priv_key is None:
+        to_jid = menu_data['jid']
+
+        # checking for private key existence is not needed, context checking is enough
+        otrctx = self.context_manager.getContextForUser(to_jid, start=False)
+        if otrctx is None or otrctx.state != otr.context.STATE_ENCRYPTED:
             not_available()
             return
-
-        def cb(to_jid):
-            otrctx = self.context_manager.getContextForUser(to_jid, start=False)
-            if otrctx is None:
-                not_available()
-                return
-            otr_version = otrctx.getUsedVersion()
-            if otr_version == otr.context.OTR_VERSION_2:
-                otrctx.fingerprintAuthCb()
-            elif otr_version == otr.context.OTR_VERSION_3:
-                otrctx.smpAuthCb('question', None, 'asked')
-            else:
-                not_available()
-
-        try:
-            to_jid = menu_data['jid']
-            self.fixResource(to_jid, cb)
-        except KeyError:
-            log.error(_("jid key is not present !"))
-            return None
+        otr_version = otrctx.getUsedVersion()
+        if otr_version == otr.context.OTR_VERSION_2:
+            otrctx.fingerprintAuthCb()
+        elif otr_version == otr.context.OTR_VERSION_3:
+            otrctx.smpAuthCb('question', None, 'asked')
+        else:
+            not_available()
 
     def _dropPrivkey(self, menu_data, profile):
         """Drop our private Key
@@ -608,21 +570,13 @@
             dialog.InfoDialog(KEY_NA_TITLE, KEY_NA_TXT, AddStyleName="maxWidthLimit").show()
             return
 
-        def cb(to_jid):
-            def dropKey(confirm):
-                if confirm:
-                    # we end all sessions
-                    for context in self.context_manager.contexts.values():
-                        context.disconnect()
-                    self.context_manager.contexts.clear()
-                    self.context_manager.account.privkey = None
-                    dialog.InfoDialog(KEY_TITLE, KEY_DROPPED_TXT, AddStyleName="maxWidthLimit").show()
+        def dropKey(confirm):
+            if confirm:
+                # we end all sessions
+                for context in self.context_manager.contexts.values():
+                    context.disconnect()
+                self.context_manager.contexts.clear()
+                self.context_manager.account.privkey = None
+                dialog.InfoDialog(KEY_TITLE, KEY_DROPPED_TXT, AddStyleName="maxWidthLimit").show()
 
-            dialog.ConfirmDialog(dropKey, KEY_DROP_TXT.format(eol=DIALOG_EOL), KEY_DROP_TITLE, AddStyleName="maxWidthLimit").show()
-
-        try:
-            to_jid = menu_data['jid']
-            self.fixResource(to_jid, cb)
-        except KeyError:
-            log.error(_("jid key is not present !"))
-            return None
+        dialog.ConfirmDialog(dropKey, KEY_DROP_TXT.format(eol=DIALOG_EOL), KEY_DROP_TITLE, AddStyleName="maxWidthLimit").show()