changeset 2138:6e509ee853a8

plugin OTR, core; use of new sendMessage + OTR mini refactoring: - new client.sendMessage method is used instead of sendMessageToStream - client.feedback is used in OTR - OTR now add message processing hints and carbon private element as recommanded by XEP-0364. Explicit Message Encryption is still TODO - OTR use the new sendMessageFinish trigger, this has a number of advantages: * there is little risk that OTR is skipped by other plugins (they have to use client.sendMessage as recommanded) * being at the end of the chain, OTR can check and remove any HTML or other leaking elements * OTR doesn't have to skip other plugins anymore, this means that things like delivery receipts are now working with OTR (but because there is not full stanza encryption, they can leak metadata) * OTR can decide to follow storage hint by letting or deleting "history" key
author Goffi <goffi@goffi.org>
date Sun, 05 Feb 2017 15:00:01 +0100
parents 410e7a940a8b
children f8401024ab28
files src/core/sat_main.py src/plugins/plugin_sec_otr.py src/plugins/plugin_xep_0033.py src/plugins/plugin_xep_0249.py
diffstat 4 files changed, 82 insertions(+), 89 deletions(-) [+]
line wrap: on
line diff
--- a/src/core/sat_main.py	Sun Feb 05 14:55:56 2017 +0100
+++ b/src/core/sat_main.py	Sun Feb 05 15:00:01 2017 +0100
@@ -652,7 +652,7 @@
 
         pre_xml_treatments.addCallback(lambda dummy: self.generateMessageXML(data))
         pre_xml_treatments.chainDeferred(post_xml_treatments)
-        post_xml_treatments.addCallback(self.messageSendToStream, client)
+        post_xml_treatments.addCallback(client.sendMessage)
         if send_only:
             log.debug(_("Triggers, storage and echo have been inhibited by the 'send_only' parameter"))
         else:
@@ -666,15 +666,6 @@
         """A message sending can be cancelled by a plugin treatment"""
         failure.trap(exceptions.CancelError)
 
-    def messageSendToStream(self, data, client):
-        """Actualy send the message to the server
-
-        @param data: message data dictionnary
-        @param client: profile's client
-        """
-        client.send(data[u'xml'])
-        return data
-
     def messageAddToHistory(self, data, client):
         """Store message into database (for local history)
 
--- a/src/plugins/plugin_sec_otr.py	Sun Feb 05 14:55:56 2017 +0100
+++ b/src/plugins/plugin_sec_otr.py	Sun Feb 05 15:00:01 2017 +0100
@@ -37,13 +37,13 @@
 
 
 PLUGIN_INFO = {
-    "name": "OTR",
-    "import_name": "OTR",
-    "type": "SEC",
-    "protocols": [],
-    "dependencies": [],
-    "main": "OTR",
-    "handler": "no",
+    "name": u"OTR",
+    "import_name": u"OTR",
+    "type": u"SEC",
+    "protocols": [u"XEP-0364"],
+    "dependencies": [u"XEP-0280", u"XEP-0334"],
+    "main": u"OTR",
+    "handler": u"no",
     "description": _(u"""Implementation of OTR""")
 }
 
@@ -53,7 +53,7 @@
 AUTH_TXT = D_(u"To authenticate your correspondent, you need to give your below fingerprint *BY AN EXTERNAL CANAL* (i.e. not in this chat), and check that the one he gives you is the same as below. If there is a mismatch, there can be a spy between you!")
 DROP_TXT = D_(u"You private key is used to encrypt messages for your correspondent, nobody except you must know it, if you are in doubt, you should drop it!\n\nAre you sure you want to drop your private key?")
 # NO_LOG_AND = D_(u"/!\\Your history is not logged anymore, and")   # FIXME: not used at the moment
-NO_ADV_FEATURES = D_(u"Most of advanced features are disabled !")
+NO_ADV_FEATURES = D_(u"Some of advanced features are disabled !")
 
 DEFAULT_POLICY_FLAGS = {
   'ALLOW_V1':False,
@@ -79,22 +79,36 @@
             return False
 
     def inject(self, msg_str, appdata=None):
+        """Inject encrypted data in the stream
+
+        if appdata is not None, we are sending a message in sendMessageFinishTrigger
+        stanza will be injected directly if appdata is None, else we just update the element
+        and follow normal workflow
+        @param msg_str(str): encrypted message body
+        @param appdata(None, dict): None for signal message,
+            message data when an encrypted message is going to be sent
+        """
         assert isinstance(self.peer, jid.JID)
         msg = msg_str.decode('utf-8')
         client = self.user.client
-        log.debug(u'inject(%s, appdata=%s, to=%s)' % (msg, appdata, self.peer))
-        mess_data = {
-                     'from': client.jid,
-                     'to': self.peer,
-                     'uid': unicode(uuid.uuid4()),
-                     'message': {'': msg},
-                     'subject': {},
-                     'type': 'chat',
-                     'extra': {},
-                     'timestamp': time.time(),
-                    }
-        self.host.generateMessageXML(mess_data)
-        client.send(mess_data['xml'])
+        log.debug(u'injecting encrypted message to {to}'.format(to=self.peer))
+        if appdata is None:
+            mess_data = {
+                         'from': client.jid,
+                         'to': self.peer,
+                         'uid': unicode(uuid.uuid4()),
+                         'message': {'': msg},
+                         'subject': {},
+                         'type': 'chat',
+                         'extra': {},
+                         'timestamp': time.time(),
+                        }
+            self.host.generateMessageXML(mess_data)
+            client.send(mess_data['xml'])
+        else:
+            message_elt = appdata[u'xml']
+            assert message_elt.name == u'message'
+            message_elt.addElement("body", content=msg)
 
     def setState(self, state):
         client = self.user.client
@@ -129,15 +143,7 @@
             log.error(D_(u"Unknown OTR state"))
             return
 
-        self.host.bridge.messageNew(uid=unicode(uuid.uuid4()),
-                                    timestamp=time.time(),
-                                    from_jid=client.jid.full(),
-                                    to_jid=self.peer.full(),
-                                    message={u'': feedback},
-                                    subject={},
-                                    mess_type=C.MESS_TYPE_INFO,
-                                    extra={},
-                                    profile=client.profile)
+        client.feedback(self.peer, feedback)
 
     def disconnect(self):
         """Disconnect the session."""
@@ -151,7 +157,8 @@
 
 
 class Account(potr.context.Account):
-    #TODO: manage trusted keys: if a fingerprint is not used anymore, we have no way to remove it from database yet (same thing for a correspondent jid)
+    # TODO: manage trusted keys: if a fingerprint is not used anymore, we have no way to remove it from database yet (same thing for a correspondent jid)
+    # TODO: manage explicit message encryption
 
     def __init__(self, host, client):
         log.debug(u"new account: %s" % client.jid)
@@ -224,8 +231,11 @@
         self.host = host
         self.context_managers = {}
         self.skipped_profiles = set()  # FIXME: OTR should not be skipped per profile, this need to be refactored
+        self._p_hints = host.plugins[u'XEP-0334']
+        self._p_carbons = host.plugins[u'XEP-0280']
         host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000)
         host.trigger.add("messageSend", self.messageSendTrigger, priority=100000)
+        host.trigger.add("sendMessageFinish", self._sendMessageFinishTrigger)
         host.bridge.addMethod("skipOTR", ".plugin", in_sign='s', out_sign='', method=self._skipOTR)  # FIXME: must be removed, must be done on per-message basis
         host.bridge.addSignal("otrState", ".plugin", signature='sss')  # args: state, destinee_jid, profile
         host.importMenu((OTR_MENU, D_(u"Start/Refresh")), self._otrStartRefresh, security_limit=0, help_string=D_(u"Start or refresh an OTR session"), type_=C.MENU_SINGLE)
@@ -444,15 +454,7 @@
                     jid = from_jid.full()))
 
                 feedback=D_(u"WARNING: received unencrypted data in a supposedly encrypted context"),
-                self.host.bridge.messageNew(uid=unicode(uuid.uuid4()),
-                                            timestamp=time.time(),
-                                            from_jid=from_jid.full(),
-                                            to_jid=client.jid.full(),
-                                            message={u'': feedback},
-                                            subject={},
-                                            mess_type=C.MESS_TYPE_INFO,
-                                            extra={},
-                                            profile=client.profile)
+                client.feedback(from_jid.full(), feedback)
         except StopIteration:
             return data
         else:
@@ -465,7 +467,7 @@
                 data['message'] = {'':res[0].decode('utf-8')} # FIXME: Q&D fix for message refactoring, message is now a dict
                 try:
                     # we want to keep message in history, even if no store is requested in message hints
-                    del message[u'history']
+                    del data[u'history']
                 except KeyError:
                     pass
                 # TODO: add skip history as an option, but by default we don't skip it
@@ -503,51 +505,50 @@
             post_treat.addCallback(self._receivedTreatment, client)
         return True
 
-    def _messageSendOTR(self, mess_data, client):
-        to_jid = copy.copy(mess_data['to'])
-        if not to_jid.resource:
-            to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: full jid may not be known
-        otrctx = client._otr_context_manager.getContextForUser(to_jid)
-        if otrctx.state != potr.context.STATE_PLAINTEXT:
-            if otrctx.state == potr.context.STATE_ENCRYPTED:
-                log.debug(u"encrypting message")
-                try:
-                    msg = mess_data['message']['']
-                except KeyError:
-                    try:
-                        msg = mess_data['message'].itervalues().next()
-                    except StopIteration:
-                        log.warning(u"No message found")
-                        for key, value in mess_data['extra'].iteritems():
-                            if key.startswith('rich') or key.startswith('xhtml'):
-                                log.error(u'received rich content while OTR encryption is activated, cancelling')
-                                raise failure.Failure(exceptions.CancelError())
-                        return mess_data
-                otrctx.sendMessage(0, msg.encode('utf-8'))
-                self.host.messageAddToHistory(mess_data, client)
-                self.host.messageSendToBridge(mess_data, client)
+    def _sendMessageFinishTrigger(self, client, mess_data):
+        if not 'OTR' in mess_data:
+            return
+        otrctx = mess_data['OTR']
+        message_elt = mess_data['xml']
+        to_jid = mess_data['to']
+        if otrctx.state == potr.context.STATE_ENCRYPTED:
+            log.debug(u"encrypting message")
+            body = None
+            for child in list(message_elt.children):
+                if child.name == 'body':
+                    # we remove all unencrypted body,
+                    # and will only encrypt the first one
+                    if body is None:
+                        body = child
+                    message_elt.children.remove(child)
+                elif child.name == 'html':
+                    # we don't want any XHTML-IM element
+                    message_elt.children.remove(child)
+            if body is None:
+                log.warning(u"No message found")
             else:
-                feedback = D_(u"Your message was not sent because your correspondent closed the encrypted conversation on his/her side. Either close your own side, or refresh the session.")
-                self.host.bridge.messageNew(uid=unicode(uuid.uuid4()),
-                                            timestamp=time.time(),
-                                            from_jid=to_jid.full(),
-                                            to_jid=client.jid.full(),
-                                            message={u'': feedback},
-                                            subject={},
-                                            mess_type=C.MESS_TYPE_INFO,
-                                            extra={},
-                                            profile=client.profile)
-            # we stop treatment here for OTR, as message is already sent
-            raise failure.Failure(exceptions.CancelError())
+                self._p_carbons.setPrivate(message_elt)
+                otrctx.sendMessage(0, unicode(body).encode('utf-8'), appdata=mess_data)
         else:
-            return mess_data
+            feedback = D_(u"Your message was not sent because your correspondent closed the encrypted conversation on his/her side. "
+                          u"Either close your own side, or refresh the session.")
+            client.feedback(to_jid.full(), feedback)
 
     def messageSendTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
         if mess_data['type'] == 'groupchat':
             return True
         if client.profile in self.skipped_profiles:  # FIXME: should not be done on a per-profile basis
             return True
-        pre_xml_treatments.addCallback(self._messageSendOTR, client)
+        to_jid = copy.copy(mess_data['to'])
+        if not to_jid.resource:
+            to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: full jid may not be known
+        otrctx = client._otr_context_manager.getContextForUser(to_jid)
+        if otrctx.state != potr.context.STATE_PLAINTEXT:
+            self._p_hints.addHint(mess_data, self._p_hints.HINT_NO_COPY)
+            self._p_hints.addHint(mess_data, self._p_hints.HINT_NO_PERMANENT_STORE)
+            mess_data['OTR'] = otrctx  # this indicate that encryption is needed in sendMessageFinish trigger
+            if not mess_data['to'].resource:  # if not resource was given, we force it here
+                mess_data['to'] = to_jid
         return True
 
     def _presenceReceivedTrigger(self, entity, show, priority, statuses, profile):
--- a/src/plugins/plugin_xep_0033.py	Sun Feb 05 14:55:56 2017 +0100
+++ b/src/plugins/plugin_xep_0033.py	Sun Feb 05 15:00:01 2017 +0100
@@ -124,7 +124,7 @@
             client = self.host.profiles[profile]
             d = defer.Deferred()
             if not skip_send:
-                d.addCallback(self.host.messageSendToStream, client)
+                d.addCallback(client.sendMessage)
             d.addCallback(self.host.messageAddToHistory, client)
             d.addCallback(self.host.messageSendToBridge, client)
             d.addErrback(lambda failure: failure.trap(exceptions.CancelError))
--- a/src/plugins/plugin_xep_0249.py	Sun Feb 05 14:55:56 2017 +0100
+++ b/src/plugins/plugin_xep_0249.py	Sun Feb 05 15:00:01 2017 +0100
@@ -119,6 +119,7 @@
                 log.warning(u"Ignoring invalid invite option: {}".format(key))
                 continue
             x_elt[key] = value
+        # there is not body in this message, so we can use directly send()
         client.send(message)
 
     def _accept(self, room_jid, profile_key=C.PROF_KEY_NONE):