comparison src/plugins/plugin_sec_otr.py @ 2144:1d3f73e065e1

core, jp: component handling + client handling refactoring: - SàT can now handle components - plugin have now a "modes" key in PLUGIN_INFO where they declare if they can be used with clients and or components. They default to be client only. - components are really similar to clients, but with some changes in behaviour: * component has "entry point", which is a special plugin with a componentStart method, which is called just after component is connected * trigger end with a different suffixes (e.g. profileConnected vs profileConnectedComponent), so a plugin which manage both clients and components can have different workflow * for clients, only triggers of plugins handling client mode are launched * for components, only triggers of plugins needed in dependencies are launched. They all must handle component mode. * component have a sendHistory attribute (False by default) which can be set to True to allow saving sent messages into history * for convenience, "client" is still used in method even if it can now be a component * a new "component" boolean attribute tells if we have a component or a client * components have to add themselve Message protocol * roster and presence protocols are not added for components * component default port is 5347 (which is Prosody's default port) - asyncCreateProfile has been renamed for profileCreate, both to follow new naming convention and to prepare the transition to fully asynchronous bridge - createProfile has a new "component" attribute. When used to create a component, it must be set to a component entry point - jp: added --component argument to profile/create - disconnect bridge method is now asynchronous, this way frontends can know when disconnection is finished - new PI_* constants for PLUGIN_INFO values (not used everywhere yet) - client/component connection workflow has been moved to their classes instead of being a host methods - host.messageSend is now client.sendMessage, and former client.sendMessage is now client.sendMessageData. - identities are now handled in client.identities list, so it can be updated dynamically by plugins (in the future, frontends should be able to update them too through bridge) - profileConnecting* profileConnected* profileDisconnected* and getHandler now all use client instead of profile
author Goffi <goffi@goffi.org>
date Sun, 12 Feb 2017 17:55:43 +0100
parents 6e509ee853a8
children 33c8c4973743
comparison
equal deleted inserted replaced
2143:c3cac21157d4 2144:1d3f73e065e1
79 return False 79 return False
80 80
81 def inject(self, msg_str, appdata=None): 81 def inject(self, msg_str, appdata=None):
82 """Inject encrypted data in the stream 82 """Inject encrypted data in the stream
83 83
84 if appdata is not None, we are sending a message in sendMessageFinishTrigger 84 if appdata is not None, we are sending a message in sendMessageDataTrigger
85 stanza will be injected directly if appdata is None, else we just update the element 85 stanza will be injected directly if appdata is None, else we just update the element
86 and follow normal workflow 86 and follow normal workflow
87 @param msg_str(str): encrypted message body 87 @param msg_str(str): encrypted message body
88 @param appdata(None, dict): None for signal message, 88 @param appdata(None, dict): None for signal message,
89 message data when an encrypted message is going to be sent 89 message data when an encrypted message is going to be sent
101 'subject': {}, 101 'subject': {},
102 'type': 'chat', 102 'type': 'chat',
103 'extra': {}, 103 'extra': {},
104 'timestamp': time.time(), 104 'timestamp': time.time(),
105 } 105 }
106 self.host.generateMessageXML(mess_data) 106 client.generateMessageXML(mess_data)
107 client.send(mess_data['xml']) 107 client.send(mess_data['xml'])
108 else: 108 else:
109 message_elt = appdata[u'xml'] 109 message_elt = appdata[u'xml']
110 assert message_elt.name == u'message' 110 assert message_elt.name == u'message'
111 message_elt.addElement("body", content=msg) 111 message_elt.addElement("body", content=msg)
232 self.context_managers = {} 232 self.context_managers = {}
233 self.skipped_profiles = set() # FIXME: OTR should not be skipped per profile, this need to be refactored 233 self.skipped_profiles = set() # FIXME: OTR should not be skipped per profile, this need to be refactored
234 self._p_hints = host.plugins[u'XEP-0334'] 234 self._p_hints = host.plugins[u'XEP-0334']
235 self._p_carbons = host.plugins[u'XEP-0280'] 235 self._p_carbons = host.plugins[u'XEP-0280']
236 host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000) 236 host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000)
237 host.trigger.add("messageSend", self.messageSendTrigger, priority=100000) 237 host.trigger.add("sendMessage", self.sendMessageTrigger, priority=100000)
238 host.trigger.add("sendMessageFinish", self._sendMessageFinishTrigger) 238 host.trigger.add("sendMessageData", self._sendMessageDataTrigger)
239 host.bridge.addMethod("skipOTR", ".plugin", in_sign='s', out_sign='', method=self._skipOTR) # FIXME: must be removed, must be done on per-message basis 239 host.bridge.addMethod("skipOTR", ".plugin", in_sign='s', out_sign='', method=self._skipOTR) # FIXME: must be removed, must be done on per-message basis
240 host.bridge.addSignal("otrState", ".plugin", signature='sss') # args: state, destinee_jid, profile 240 host.bridge.addSignal("otrState", ".plugin", signature='sss') # args: state, destinee_jid, profile
241 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) 241 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)
242 host.importMenu((OTR_MENU, D_(u"End session")), self._otrSessionEnd, security_limit=0, help_string=D_(u"Finish an OTR session"), type_=C.MENU_SINGLE) 242 host.importMenu((OTR_MENU, D_(u"End session")), self._otrSessionEnd, security_limit=0, help_string=D_(u"Finish an OTR session"), type_=C.MENU_SINGLE)
243 host.importMenu((OTR_MENU, D_(u"Authenticate")), self._otrAuthenticate, security_limit=0, help_string=D_(u"Authenticate user/see your fingerprint"), type_=C.MENU_SINGLE) 243 host.importMenu((OTR_MENU, D_(u"Authenticate")), self._otrAuthenticate, security_limit=0, help_string=D_(u"Authenticate user/see your fingerprint"), type_=C.MENU_SINGLE)
253 # for message received, profile wide hook may be need, but client 253 # for message received, profile wide hook may be need, but client
254 # should be used anyway instead of a class attribute 254 # should be used anyway instead of a class attribute
255 self.skipped_profiles.add(profile) 255 self.skipped_profiles.add(profile)
256 256
257 @defer.inlineCallbacks 257 @defer.inlineCallbacks
258 def profileConnected(self, profile): 258 def profileConnected(self, client):
259 if profile in self.skipped_profiles: 259 if client.profile in self.skipped_profiles:
260 return 260 return
261 client = self.host.getClient(profile)
262 ctxMng = client._otr_context_manager = ContextManager(self.host, client) 261 ctxMng = client._otr_context_manager = ContextManager(self.host, client)
263 client._otr_data = persistent.PersistentBinaryDict(NS_OTR, profile) 262 client._otr_data = persistent.PersistentBinaryDict(NS_OTR, client.profile)
264 yield client._otr_data.load() 263 yield client._otr_data.load()
265 encrypted_priv_key = client._otr_data.get(PRIVATE_KEY, None) 264 encrypted_priv_key = client._otr_data.get(PRIVATE_KEY, None)
266 if encrypted_priv_key is not None: 265 if encrypted_priv_key is not None:
267 priv_key = yield self.host.memory.decryptValue(encrypted_priv_key, profile) 266 priv_key = yield self.host.memory.decryptValue(encrypted_priv_key, client.profile)
268 ctxMng.account.privkey = potr.crypt.PK.parsePrivateKey(priv_key.decode('hex'))[0] 267 ctxMng.account.privkey = potr.crypt.PK.parsePrivateKey(priv_key.decode('hex'))[0]
269 else: 268 else:
270 ctxMng.account.privkey = None 269 ctxMng.account.privkey = None
271 ctxMng.account.loadTrusts() 270 ctxMng.account.loadTrusts()
272 271
273 def profileDisconnected(self, profile): 272 def profileDisconnected(self, client):
274 if profile in self.skipped_profiles: 273 if client.profile in self.skipped_profiles:
275 self.skipped_profiles.remove(profile) 274 self.skipped_profiles.remove(client.profile)
276 return 275 return
277 client = self.host.getClient(profile)
278 for context in client._otr_context_manager.contexts.values(): 276 for context in client._otr_context_manager.contexts.values():
279 context.disconnect() 277 context.disconnect()
280 del client._otr_context_manager 278 del client._otr_context_manager
281 279
282 def _otrStartRefresh(self, menu_data, profile): 280 def _otrStartRefresh(self, menu_data, profile):
452 if otrctx.state == potr.context.STATE_ENCRYPTED: 450 if otrctx.state == potr.context.STATE_ENCRYPTED:
453 log.warning(u"Received unencrypted message in an encrypted context (from {jid})".format( 451 log.warning(u"Received unencrypted message in an encrypted context (from {jid})".format(
454 jid = from_jid.full())) 452 jid = from_jid.full()))
455 453
456 feedback=D_(u"WARNING: received unencrypted data in a supposedly encrypted context"), 454 feedback=D_(u"WARNING: received unencrypted data in a supposedly encrypted context"),
457 client.feedback(from_jid.full(), feedback) 455 client.feedback(from_jid, feedback)
458 except StopIteration: 456 except StopIteration:
459 return data 457 return data
460 else: 458 else:
461 encrypted = True 459 encrypted = True
462 460
503 post_treat.addCallback(self._receivedTreatmentForSkippedProfiles) 501 post_treat.addCallback(self._receivedTreatmentForSkippedProfiles)
504 else: 502 else:
505 post_treat.addCallback(self._receivedTreatment, client) 503 post_treat.addCallback(self._receivedTreatment, client)
506 return True 504 return True
507 505
508 def _sendMessageFinishTrigger(self, client, mess_data): 506 def _sendMessageDataTrigger(self, client, mess_data):
509 if not 'OTR' in mess_data: 507 if not 'OTR' in mess_data:
510 return 508 return
511 otrctx = mess_data['OTR'] 509 otrctx = mess_data['OTR']
512 message_elt = mess_data['xml'] 510 message_elt = mess_data['xml']
513 to_jid = mess_data['to'] 511 to_jid = mess_data['to']
530 self._p_carbons.setPrivate(message_elt) 528 self._p_carbons.setPrivate(message_elt)
531 otrctx.sendMessage(0, unicode(body).encode('utf-8'), appdata=mess_data) 529 otrctx.sendMessage(0, unicode(body).encode('utf-8'), appdata=mess_data)
532 else: 530 else:
533 feedback = D_(u"Your message was not sent because your correspondent closed the encrypted conversation on his/her side. " 531 feedback = D_(u"Your message was not sent because your correspondent closed the encrypted conversation on his/her side. "
534 u"Either close your own side, or refresh the session.") 532 u"Either close your own side, or refresh the session.")
535 client.feedback(to_jid.full(), feedback) 533 log.warning(_(u"Message discarded because closed encryption channel"))
536 534 client.feedback(to_jid, feedback)
537 def messageSendTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments): 535 raise failure.Failure(exceptions.CancelError(u'Cancelled by OTR plugin'))
536
537 def sendMessageTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
538 if mess_data['type'] == 'groupchat': 538 if mess_data['type'] == 'groupchat':
539 return True 539 return True
540 if client.profile in self.skipped_profiles: # FIXME: should not be done on a per-profile basis 540 if client.profile in self.skipped_profiles: # FIXME: should not be done on a per-profile basis
541 return True 541 return True
542 to_jid = copy.copy(mess_data['to']) 542 to_jid = copy.copy(mess_data['to'])
544 to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: full jid may not be known 544 to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: full jid may not be known
545 otrctx = client._otr_context_manager.getContextForUser(to_jid) 545 otrctx = client._otr_context_manager.getContextForUser(to_jid)
546 if otrctx.state != potr.context.STATE_PLAINTEXT: 546 if otrctx.state != potr.context.STATE_PLAINTEXT:
547 self._p_hints.addHint(mess_data, self._p_hints.HINT_NO_COPY) 547 self._p_hints.addHint(mess_data, self._p_hints.HINT_NO_COPY)
548 self._p_hints.addHint(mess_data, self._p_hints.HINT_NO_PERMANENT_STORE) 548 self._p_hints.addHint(mess_data, self._p_hints.HINT_NO_PERMANENT_STORE)
549 mess_data['OTR'] = otrctx # this indicate that encryption is needed in sendMessageFinish trigger 549 mess_data['OTR'] = otrctx # this indicate that encryption is needed in sendMessageData trigger
550 if not mess_data['to'].resource: # if not resource was given, we force it here 550 if not mess_data['to'].resource: # if not resource was given, we force it here
551 mess_data['to'] = to_jid 551 mess_data['to'] = to_jid
552 return True 552 return True
553 553
554 def _presenceReceivedTrigger(self, entity, show, priority, statuses, profile): 554 def _presenceReceivedTrigger(self, entity, show, priority, statuses, profile):