comparison src/plugins/plugin_sec_otr.py @ 1955:633b5c21aefd

backend, frontend: messages refactoring (huge commit, not finished): /!\ database schema has been modified, do a backup before updating message have been refactored, here are the main changes: - languages are now handled - all messages have an uid (internal to SàT) - message updating is anticipated - subject is now first class - new naming scheme is used newMessage => messageNew, getHistory => historyGet, sendMessage => messageSend - minimal compatibility refactoring in quick_frontend/Primitivus, better refactoring should follow - threads handling - delayed messages are saved into history - info messages may also be saved in history (e.g. to keep track of people joining/leaving a room) - duplicate messages should be avoided - historyGet return messages in right order, no need to sort again - plugins have been updated to follow new features, some of them need to be reworked (e.g. OTR) - XEP-0203 (Delayed Delivery) is now fully handled in core, the plugin just handle disco and creation of a delay element - /!\ jp and Libervia are currently broken, as some features of Primitivus It has been put in one huge commit to avoid breaking messaging between changes. This is the main part of message refactoring, other commits will follow to take profit of the new features/behaviour.
author Goffi <goffi@goffi.org>
date Tue, 24 May 2016 22:11:04 +0200
parents 2daf7b4c6756
children a2bc5089c2eb
comparison
equal deleted inserted replaced
1943:ccfe45302a5c 1955:633b5c21aefd
30 from twisted.python import failure 30 from twisted.python import failure
31 from twisted.internet import defer 31 from twisted.internet import defer
32 from sat.memory import persistent 32 from sat.memory import persistent
33 import potr 33 import potr
34 import copy 34 import copy
35 import time
36 import uuid
35 37
36 NS_OTR = "otr_plugin" 38 NS_OTR = "otr_plugin"
37 PRIVATE_KEY = "PRIVATE KEY" 39 PRIVATE_KEY = "PRIVATE KEY"
38 MAIN_MENU = D_('OTR') 40 MAIN_MENU = D_('OTR')
39 AUTH_TXT = D_("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!") 41 AUTH_TXT = D_("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!")
71 def inject(self, msg_str, appdata=None): 73 def inject(self, msg_str, appdata=None):
72 assert isinstance(self.peer, jid.JID) 74 assert isinstance(self.peer, jid.JID)
73 msg = msg_str.decode('utf-8') 75 msg = msg_str.decode('utf-8')
74 client = self.user.client 76 client = self.user.client
75 log.debug(u'inject(%s, appdata=%s, to=%s)' % (msg, appdata, self.peer)) 77 log.debug(u'inject(%s, appdata=%s, to=%s)' % (msg, appdata, self.peer))
76 mess_data = {'message': msg, 78 mess_data = {
79 'from': client.jid,
80 'to': self.peer,
81 'uid': unicode(uuid.uuid4()),
82 'message': {'': msg},
83 'subject': {},
77 'type': 'chat', 84 'type': 'chat',
78 'from': client.jid, 85 'extra': {},
79 'to': self.peer, 86 'timestamp': time.time(),
80 'subject': None,
81 } 87 }
82 self.host.generateMessageXML(mess_data) 88 self.host.generateMessageXML(mess_data)
83 client.xmlstream.send(mess_data['xml']) 89 client.xmlstream.send(mess_data['xml'])
84 90
85 def setState(self, state): 91 def setState(self, state):
105 else: 111 else:
106 log.error(_(u"Unknown OTR state")) 112 log.error(_(u"Unknown OTR state"))
107 return 113 return
108 114
109 client = self.user.client 115 client = self.user.client
110 self.host.bridge.newMessage(client.jid.full(), 116 self.host.bridge.messageNew(client.jid.full(),
111 feedback, 117 feedback,
112 mess_type=C.MESS_TYPE_INFO, 118 mess_type=C.MESS_TYPE_INFO,
113 to_jid=self.peer.full(), 119 to_jid=self.peer.full(),
114 extra={}, 120 extra={},
115 profile=client.profile) 121 profile=client.profile)
200 self._fixPotr() # FIXME: to be removed when potr will be fixed 206 self._fixPotr() # FIXME: to be removed when potr will be fixed
201 self.host = host 207 self.host = host
202 self.context_managers = {} 208 self.context_managers = {}
203 self.skipped_profiles = set() 209 self.skipped_profiles = set()
204 host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000) 210 host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000)
205 host.trigger.add("sendMessage", self.sendMessageTrigger, priority=100000) 211 host.trigger.add("messageSend", self.messageSendTrigger, priority=100000)
206 host.bridge.addMethod("skipOTR", ".plugin", in_sign='s', out_sign='', method=self._skipOTR) 212 host.bridge.addMethod("skipOTR", ".plugin", in_sign='s', out_sign='', method=self._skipOTR)
207 host.importMenu((MAIN_MENU, D_("Start/Refresh")), self._startRefresh, security_limit=0, help_string=D_("Start or refresh an OTR session"), type_=C.MENU_SINGLE) 213 host.importMenu((MAIN_MENU, D_("Start/Refresh")), self._startRefresh, security_limit=0, help_string=D_("Start or refresh an OTR session"), type_=C.MENU_SINGLE)
208 host.importMenu((MAIN_MENU, D_("End session")), self._endSession, security_limit=0, help_string=D_("Finish an OTR session"), type_=C.MENU_SINGLE) 214 host.importMenu((MAIN_MENU, D_("End session")), self._endSession, security_limit=0, help_string=D_("Finish an OTR session"), type_=C.MENU_SINGLE)
209 host.importMenu((MAIN_MENU, D_("Authenticate")), self._authenticate, security_limit=0, help_string=D_("Authenticate user/see your fingerprint"), type_=C.MENU_SINGLE) 215 host.importMenu((MAIN_MENU, D_("Authenticate")), self._authenticate, security_limit=0, help_string=D_("Authenticate user/see your fingerprint"), type_=C.MENU_SINGLE)
210 host.importMenu((MAIN_MENU, D_("Drop private key")), self._dropPrivKey, security_limit=0, type_=C.MENU_SINGLE) 216 host.importMenu((MAIN_MENU, D_("Drop private key")), self._dropPrivKey, security_limit=0, type_=C.MENU_SINGLE)
273 to_jid.resource = self.host.memory.getMainResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored 279 to_jid.resource = self.host.memory.getMainResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored
274 except KeyError: 280 except KeyError:
275 log.error(_("jid key is not present !")) 281 log.error(_("jid key is not present !"))
276 return defer.fail(exceptions.DataError) 282 return defer.fail(exceptions.DataError)
277 otrctx = self.context_managers[profile].getContextForUser(to_jid) 283 otrctx = self.context_managers[profile].getContextForUser(to_jid)
278 query = otrctx.sendMessage(0, '?OTRv?') 284 query = otrctx.messageSend(0, '?OTRv?')
279 otrctx.inject(query) 285 otrctx.inject(query)
280 return {} 286 return {}
281 287
282 def _endSession(self, menu_data, profile): 288 def _endSession(self, menu_data, profile):
283 """End an OTR session 289 """End an OTR session
404 log.debug(u"_receivedTreatment [from_jid = %s]" % from_jid) 410 log.debug(u"_receivedTreatment [from_jid = %s]" % from_jid)
405 otrctx = self.context_managers[profile].getContextForUser(from_jid) 411 otrctx = self.context_managers[profile].getContextForUser(from_jid)
406 encrypted = True 412 encrypted = True
407 413
408 try: 414 try:
409 res = otrctx.receiveMessage(data['body'].encode('utf-8')) 415 message = data['message'].itervalues().next() # FIXME: Q&D fix for message refactoring, message is now a dict
416 res = otrctx.receiveMessage(message.encode('utf-8'))
410 except potr.context.UnencryptedMessage: 417 except potr.context.UnencryptedMessage:
411 if otrctx.state == potr.context.STATE_ENCRYPTED: 418 if otrctx.state == potr.context.STATE_ENCRYPTED:
412 log.warning(u"Received unencrypted message in an encrypted context (from %(jid)s)" % {'jid': from_jid.full()}) 419 log.warning(u"Received unencrypted message in an encrypted context (from %(jid)s)" % {'jid': from_jid.full()})
413 client = self.host.getClient(profile) 420 client = self.host.getClient(profile)
414 self.host.bridge.newMessage(from_jid.full(), 421 self.host.bridge.messageNew(from_jid.full(),
415 _(u"WARNING: received unencrypted data in a supposedly encrypted context"), 422 _(u"WARNING: received unencrypted data in a supposedly encrypted context"),
416 mess_type=C.MESS_TYPE_INFO, 423 mess_type=C.MESS_TYPE_INFO,
417 to_jid=client.jid.full(), 424 to_jid=client.jid.full(),
418 extra={}, 425 extra={},
419 profile=client.profile) 426 profile=client.profile)
420 encrypted = False 427 encrypted = False
428 except StopIteration:
429 return data
421 430
422 if not encrypted: 431 if not encrypted:
423 return data 432 return data
424 else: 433 else:
425 if res[0] != None: 434 if res[0] != None:
426 # decrypted messages handling. 435 # decrypted messages handling.
427 # receiveMessage() will return a tuple, the first part of which will be the decrypted message 436 # receiveMessage() will return a tuple, the first part of which will be the decrypted message
428 data['body'] = res[0].decode('utf-8') 437 data['message'] = {'':res[0].decode('utf-8')} # FIXME: Q&D fix for message refactoring, message is now a dict
429 raise failure.Failure(exceptions.SkipHistory()) # we send the decrypted message to frontends, but we don't want it in history 438 raise failure.Failure(exceptions.SkipHistory()) # we send the decrypted message to frontends, but we don't want it in history
430 else: 439 else:
431 raise failure.Failure(exceptions.CancelError()) # no message at all (no history, no signal) 440 raise failure.Failure(exceptions.CancelError()) # no message at all (no history, no signal)
432 441
433 def _receivedTreatmentForSkippedProfiles(self, data, profile): 442 def _receivedTreatmentForSkippedProfiles(self, data, profile):
434 """This profile must be skipped because the frontend manages OTR itself, 443 """This profile must be skipped because the frontend manages OTR itself,
435 but we still need to check if the message must be stored in history or not""" 444 but we still need to check if the message must be stored in history or not"""
436 body = data['body'].encode('utf-8') 445 try:
437 if body.startswith(potr.proto.OTRTAG): 446 message = data['message'].itervalues().next().encode('utf-8') # FIXME: Q&D fix for message refactoring, message is now a dict
447 except StopIteration:
448 return data
449 if message.startswith(potr.proto.OTRTAG):
438 raise failure.Failure(exceptions.SkipHistory()) 450 raise failure.Failure(exceptions.SkipHistory())
439 return data 451 return data
440 452
441 def MessageReceivedTrigger(self, message, post_treat, profile): 453 def MessageReceivedTrigger(self, client, message, post_treat):
454 profile = client.profile
442 if profile in self.skipped_profiles: 455 if profile in self.skipped_profiles:
443 post_treat.addCallback(self._receivedTreatmentForSkippedProfiles, profile) 456 post_treat.addCallback(self._receivedTreatmentForSkippedProfiles, profile)
444 else: 457 else:
445 post_treat.addCallback(self._receivedTreatment, profile) 458 post_treat.addCallback(self._receivedTreatment, profile)
446 return True 459 return True
447 460
448 def sendMessageTrigger(self, mess_data, pre_xml_treatments, post_xml_treatments, profile): 461 def messageSendTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
462 profile = client.profile
449 if profile in self.skipped_profiles: 463 if profile in self.skipped_profiles:
450 return True 464 return True
451 to_jid = copy.copy(mess_data['to']) 465 to_jid = copy.copy(mess_data['to'])
452 if mess_data['type'] != 'groupchat' and not to_jid.resource: 466 if mess_data['type'] != 'groupchat' and not to_jid.resource:
453 to_jid.resource = self.host.memory.getMainResource(to_jid, profile) # FIXME: it's dirty, but frontends don't manage resources correctly now, refactoring is planed 467 to_jid.resource = self.host.memory.getMainResource(to_jid, profile) # FIXME: it's dirty, but frontends don't manage resources correctly now, refactoring is planed
454 otrctx = self.context_managers[profile].getContextForUser(to_jid) 468 otrctx = self.context_managers[profile].getContextForUser(to_jid)
455 if mess_data['type'] != 'groupchat' and otrctx.state != potr.context.STATE_PLAINTEXT: 469 if mess_data['type'] != 'groupchat' and otrctx.state != potr.context.STATE_PLAINTEXT:
456 if otrctx.state == potr.context.STATE_ENCRYPTED: 470 if otrctx.state == potr.context.STATE_ENCRYPTED:
457 log.debug(u"encrypting message") 471 log.debug(u"encrypting message")
458 otrctx.sendMessage(0, mess_data['message'].encode('utf-8')) 472 try:
459 client = self.host.getClient(profile) 473 msg = mess_data['message']['']
474 except KeyError:
475 try:
476 msg = mess_data['message'].itervalues().next()
477 except StopIteration:
478 log.warning(u"No message found")
479 return False
480 otrctx.sendMessage(0, msg.encode('utf-8'))
460 self.host.sendMessageToBridge(mess_data, client) 481 self.host.sendMessageToBridge(mess_data, client)
461 else: 482 else:
462 feedback = D_("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.") 483 feedback = D_("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.")
463 client = self.host.getClient(profile) 484 self.host.bridge.messageNew(to_jid.full(),
464 self.host.bridge.newMessage(to_jid.full(),
465 feedback, 485 feedback,
466 mess_type=C.MESS_TYPE_INFO, 486 mess_type=C.MESS_TYPE_INFO,
467 to_jid=client.jid.full(), 487 to_jid=client.jid.full(),
468 extra={}, 488 extra={},
469 profile=client.profile) 489 profile=client.profile)