Update the OTR plugin
make it more restrictive and give some hints
This commit is contained in:
parent
7d3403d288
commit
31fc9d00f5
2 changed files with 69 additions and 59 deletions
120
plugins/otr.py
120
plugins/otr.py
|
@ -44,11 +44,26 @@ Install the python module:
|
||||||
You can also use pip in a virtualenv (built-in as pyvenv_ with python since 3.3)
|
You can also use pip in a virtualenv (built-in as pyvenv_ with python since 3.3)
|
||||||
with the requirements.txt at the root of the poezio directory.
|
with the requirements.txt at the root of the poezio directory.
|
||||||
|
|
||||||
|
Important details
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
The OTR session is considered for a full JID (e.g. toto@example/**client1**),
|
||||||
|
but the trust is set with a bare JID (e.g. toto@example). This is important
|
||||||
|
in the case of Private Chats (in a chatroom), since you cannot always get the
|
||||||
|
real JID of your contact (or check if the same nick is used by different people).
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
This also means that you cannot have an OTR session in the "common"
|
||||||
|
conversation tab, which is not locked to a specific JID. After activating
|
||||||
|
the plugin, you need to open a session with a full JID to be able to use
|
||||||
|
OTR.
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
-----
|
-----
|
||||||
|
|
||||||
Command added to Conversation Tabs and Private Tabs:
|
Command added to Static Conversation Tabs (opened with ``/message foo@bar/baz`` or
|
||||||
|
by expanding a contact in the roster) and Private Tabs:
|
||||||
|
|
||||||
.. glossary::
|
.. glossary::
|
||||||
|
|
||||||
|
@ -161,14 +176,6 @@ Configuration
|
||||||
The :term:`require_encryption`, :term:`decode_xhtml`, :term:`decode_entities`
|
The :term:`require_encryption`, :term:`decode_xhtml`, :term:`decode_entities`
|
||||||
and :term:`log` configuration parameters are tab-specific.
|
and :term:`log` configuration parameters are tab-specific.
|
||||||
|
|
||||||
Important details
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
The OTR session is considered for a full JID (e.g. toto@example/**client1**),
|
|
||||||
but the trust is set with a bare JID (e.g. toto@example). This is important
|
|
||||||
in the case of Private Chats (in a chatroom), since you cannot always get the
|
|
||||||
real JID of your contact (or check if the same nick is used by different people).
|
|
||||||
|
|
||||||
.. _Off The Record messaging: http://wiki.xmpp.org/web/OTR
|
.. _Off The Record messaging: http://wiki.xmpp.org/web/OTR
|
||||||
.. _pyvenv: https://docs.python.org/3/using/scripts.html#pyvenv-creating-virtual-environments
|
.. _pyvenv: https://docs.python.org/3/using/scripts.html#pyvenv-creating-virtual-environments
|
||||||
|
|
||||||
|
@ -188,12 +195,13 @@ from potr.context import NotEncryptedError, UnencryptedMessage, ErrorReceived, N
|
||||||
STATE_ENCRYPTED, STATE_PLAINTEXT, STATE_FINISHED, Context, Account, crypt
|
STATE_ENCRYPTED, STATE_PLAINTEXT, STATE_FINISHED, Context, Account, crypt
|
||||||
|
|
||||||
from poezio import common
|
from poezio import common
|
||||||
from poezio import xhtml
|
|
||||||
from poezio import xdg
|
from poezio import xdg
|
||||||
|
from poezio import xhtml
|
||||||
from poezio.common import safeJID
|
from poezio.common import safeJID
|
||||||
from poezio.config import config
|
from poezio.config import config
|
||||||
from poezio.plugin import BasePlugin
|
from poezio.plugin import BasePlugin
|
||||||
from poezio.tabs import ConversationTab, DynamicConversationTab, PrivateTab
|
from poezio.roster import roster
|
||||||
|
from poezio.tabs import StaticConversationTab, PrivateTab
|
||||||
from poezio.theming import get_theme, dump_tuple
|
from poezio.theming import get_theme, dump_tuple
|
||||||
from poezio.decorators import command_args_parser
|
from poezio.decorators import command_args_parser
|
||||||
from poezio.core.structs import Completion
|
from poezio.core.structs import Completion
|
||||||
|
@ -234,6 +242,15 @@ MESSAGE_NOT_SENT = _('%(info)sYour message to %(jid_c)s%(jid)s%(info)s was'
|
||||||
'encrypted session.\nWait until it is established or '
|
'encrypted session.\nWait until it is established or '
|
||||||
'change your configuration.')
|
'change your configuration.')
|
||||||
|
|
||||||
|
INCOMPATIBLE_TAB = _('%(info)sYour message to %(jid_c)s%(jid)s%(info)s was'
|
||||||
|
' not sent because your configuration requires an '
|
||||||
|
'encrypted session and the current tab is a bare-jid '
|
||||||
|
'one, with which you cannot open or use an OTR session.'
|
||||||
|
' You need to open a fulljid tab with /message if you '
|
||||||
|
'want to use OTR.%(help)s')
|
||||||
|
|
||||||
|
TAB_HELP_RESOURCE = _('\nChoose the relevant one among the following:%s')
|
||||||
|
|
||||||
OTR_REQUEST = _('%(info)sOTR request to %(jid_c)s%(jid)s%(info)s sent.')
|
OTR_REQUEST = _('%(info)sOTR request to %(jid_c)s%(jid)s%(info)s sent.')
|
||||||
|
|
||||||
OTR_OWN_FPR = _('%(info)sYour OTR key fingerprint is '
|
OTR_OWN_FPR = _('%(info)sYour OTR key fingerprint is '
|
||||||
|
@ -304,6 +321,7 @@ TRUST_REMOVED = _('%(info)sYou removed %(jid_c)s%(bare_jid)s%(info)s with '
|
||||||
|
|
||||||
KEY_DROPPED = _('%(info)sPrivate key dropped.')
|
KEY_DROPPED = _('%(info)sPrivate key dropped.')
|
||||||
|
|
||||||
|
|
||||||
def hl(tab):
|
def hl(tab):
|
||||||
"""
|
"""
|
||||||
Make a tab beep and change its status.
|
Make a tab beep and change its status.
|
||||||
|
@ -363,10 +381,7 @@ class PoezioContext(Context):
|
||||||
|
|
||||||
tab = self.core.tabs.by_name(self.peer)
|
tab = self.core.tabs.by_name(self.peer)
|
||||||
if not tab:
|
if not tab:
|
||||||
tab = self.core.tabs.by_name(safeJID(self.peer).bare,
|
tab = None
|
||||||
DynamicConversationTab)
|
|
||||||
if tab and not tab.locked_resource == safeJID(self.peer).resource:
|
|
||||||
tab = None
|
|
||||||
if self.state == STATE_ENCRYPTED:
|
if self.state == STATE_ENCRYPTED:
|
||||||
if newstate == STATE_ENCRYPTED and tab:
|
if newstate == STATE_ENCRYPTED and tab:
|
||||||
log.debug('OTR conversation with %s refreshed', self.peer)
|
log.debug('OTR conversation with %s refreshed', self.peer)
|
||||||
|
@ -489,7 +504,7 @@ class Plugin(BasePlugin):
|
||||||
self.api.add_event_handler('conversation_say_after', self.on_conversation_say)
|
self.api.add_event_handler('conversation_say_after', self.on_conversation_say)
|
||||||
self.api.add_event_handler('private_say_after', self.on_conversation_say)
|
self.api.add_event_handler('private_say_after', self.on_conversation_say)
|
||||||
|
|
||||||
ConversationTab.add_information_element('otr', self.display_encryption_status)
|
StaticConversationTab.add_information_element('otr', self.display_encryption_status)
|
||||||
PrivateTab.add_information_element('otr', self.display_encryption_status)
|
PrivateTab.add_information_element('otr', self.display_encryption_status)
|
||||||
|
|
||||||
self.core.xmpp.plugin['xep_0030'].add_feature('urn:xmpp:otr:0')
|
self.core.xmpp.plugin['xep_0030'].add_feature('urn:xmpp:otr:0')
|
||||||
|
@ -514,14 +529,14 @@ class Plugin(BasePlugin):
|
||||||
'ask: Start a verification, with a question or not\n'
|
'ask: Start a verification, with a question or not\n'
|
||||||
'answer: Finish a verification\n')
|
'answer: Finish a verification\n')
|
||||||
|
|
||||||
self.api.add_tab_command(ConversationTab, 'otrsmp', self.command_smp,
|
self.api.add_tab_command(StaticConversationTab, 'otrsmp', self.command_smp,
|
||||||
help=smp_desc, usage=smp_usage, short=smp_short,
|
help=smp_desc, usage=smp_usage, short=smp_short,
|
||||||
completion=self.completion_smp)
|
completion=self.completion_smp)
|
||||||
self.api.add_tab_command(PrivateTab, 'otrsmp', self.command_smp,
|
self.api.add_tab_command(PrivateTab, 'otrsmp', self.command_smp,
|
||||||
help=smp_desc, usage=smp_usage, short=smp_short,
|
help=smp_desc, usage=smp_usage, short=smp_short,
|
||||||
completion=self.completion_smp)
|
completion=self.completion_smp)
|
||||||
|
|
||||||
self.api.add_tab_command(ConversationTab, 'otr', self.command_otr,
|
self.api.add_tab_command(StaticConversationTab, 'otr', self.command_otr,
|
||||||
help=desc, usage=usage, short=shortdesc,
|
help=desc, usage=usage, short=shortdesc,
|
||||||
completion=self.completion_otr)
|
completion=self.completion_otr)
|
||||||
self.api.add_tab_command(PrivateTab, 'otr', self.command_otr,
|
self.api.add_tab_command(PrivateTab, 'otr', self.command_otr,
|
||||||
|
@ -534,7 +549,7 @@ class Plugin(BasePlugin):
|
||||||
|
|
||||||
self.core.xmpp.plugin['xep_0030'].del_feature(feature='urn:xmpp:otr:0')
|
self.core.xmpp.plugin['xep_0030'].del_feature(feature='urn:xmpp:otr:0')
|
||||||
|
|
||||||
ConversationTab.remove_information_element('otr')
|
StaticConversationTab.remove_information_element('otr')
|
||||||
PrivateTab.remove_information_element('otr')
|
PrivateTab.remove_information_element('otr')
|
||||||
|
|
||||||
def get_context(self, jid):
|
def get_context(self, jid):
|
||||||
|
@ -734,10 +749,6 @@ class Plugin(BasePlugin):
|
||||||
def find_encrypted_context_with_matching(self, bare_jid):
|
def find_encrypted_context_with_matching(self, bare_jid):
|
||||||
"""
|
"""
|
||||||
Find an OTR session from a bare JID.
|
Find an OTR session from a bare JID.
|
||||||
|
|
||||||
Useful when a dynamic tab unlocks, which would lead to sending
|
|
||||||
unencrypted messages until it locks again, if we didn’t fallback
|
|
||||||
with this.
|
|
||||||
"""
|
"""
|
||||||
for ctx in self.contexts:
|
for ctx in self.contexts:
|
||||||
if safeJID(ctx).bare == bare_jid and self.contexts[ctx].state == STATE_ENCRYPTED:
|
if safeJID(ctx).bare == bare_jid and self.contexts[ctx].state == STATE_ENCRYPTED:
|
||||||
|
@ -748,13 +759,8 @@ class Plugin(BasePlugin):
|
||||||
"""
|
"""
|
||||||
On message sent
|
On message sent
|
||||||
"""
|
"""
|
||||||
if isinstance(tab, DynamicConversationTab) and tab.locked_resource:
|
name = tab.name
|
||||||
jid = safeJID(tab.name)
|
jid = safeJID(tab.name)
|
||||||
jid.resource = tab.locked_resource
|
|
||||||
name = jid.full
|
|
||||||
else:
|
|
||||||
name = tab.name
|
|
||||||
jid = safeJID(tab.name)
|
|
||||||
|
|
||||||
format_dict = {
|
format_dict = {
|
||||||
'jid_c': '\x19%s}' % dump_tuple(get_theme().COLOR_MUC_JID),
|
'jid_c': '\x19%s}' % dump_tuple(get_theme().COLOR_MUC_JID),
|
||||||
|
@ -762,17 +768,13 @@ class Plugin(BasePlugin):
|
||||||
'jid': name,
|
'jid': name,
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx = None
|
ctx = self.find_encrypted_context_with_matching(jid)
|
||||||
default_ctx = self.get_context(name)
|
default_ctx = self.get_context(name)
|
||||||
|
|
||||||
if isinstance(tab, DynamicConversationTab) and not tab.locked_resource:
|
|
||||||
log.debug('Unlocked tab %s found, falling back to the first encrypted chat we find.', name)
|
|
||||||
ctx = self.find_encrypted_context_with_matching(jid.bare)
|
|
||||||
|
|
||||||
if ctx is None:
|
if ctx is None:
|
||||||
ctx = default_ctx
|
ctx = default_ctx
|
||||||
|
|
||||||
if ctx and ctx.state == STATE_ENCRYPTED:
|
if is_relevant(tab) and ctx and ctx.state == STATE_ENCRYPTED:
|
||||||
ctx.sendMessage(0, msg['body'].encode('utf-8'))
|
ctx.sendMessage(0, msg['body'].encode('utf-8'))
|
||||||
if not tab.send_chat_state('active'):
|
if not tab.send_chat_state('active'):
|
||||||
tab.send_chat_state('inactive', always_send=True)
|
tab.send_chat_state('inactive', always_send=True)
|
||||||
|
@ -787,12 +789,28 @@ class Plugin(BasePlugin):
|
||||||
del msg['body']
|
del msg['body']
|
||||||
del msg['replace']
|
del msg['replace']
|
||||||
del msg['html']
|
del msg['html']
|
||||||
elif ctx and ctx.getPolicy('REQUIRE_ENCRYPTION'):
|
elif is_relevant(tab) and ctx and ctx.getPolicy('REQUIRE_ENCRYPTION'):
|
||||||
tab.add_message(MESSAGE_NOT_SENT % format_dict, typ=0)
|
warning_msg = MESSAGE_NOT_SENT % format_dict
|
||||||
|
tab.add_message(warning_msg, typ=0)
|
||||||
del msg['body']
|
del msg['body']
|
||||||
del msg['replace']
|
del msg['replace']
|
||||||
del msg['html']
|
del msg['html']
|
||||||
self.otr_start(tab, name, format_dict)
|
self.otr_start(tab, name, format_dict)
|
||||||
|
elif not is_relevant(tab) and ctx and (
|
||||||
|
ctx.state == STATE_ENCRYPTED or ctx.getPolicy('REQUIRE_ENCRYPTION')):
|
||||||
|
contact = roster[tab.name]
|
||||||
|
res = []
|
||||||
|
if contact:
|
||||||
|
res = [resource.jid for resource in contact.resources]
|
||||||
|
help_msg = ''
|
||||||
|
if res:
|
||||||
|
help_msg = TAB_HELP_RESOURCE % ''.join(('\n - /message %s' % jid) for jid in res)
|
||||||
|
format_dict['help'] = help_msg
|
||||||
|
warning_msg = INCOMPATIBLE_TAB % format_dict
|
||||||
|
tab.add_message(warning_msg, typ=0)
|
||||||
|
del msg['body']
|
||||||
|
del msg['replace']
|
||||||
|
del msg['html']
|
||||||
|
|
||||||
def display_encryption_status(self, jid):
|
def display_encryption_status(self, jid):
|
||||||
"""
|
"""
|
||||||
|
@ -818,10 +836,6 @@ class Plugin(BasePlugin):
|
||||||
action = args.pop(0)
|
action = args.pop(0)
|
||||||
tab = self.api.current_tab()
|
tab = self.api.current_tab()
|
||||||
name = tab.name
|
name = tab.name
|
||||||
if isinstance(tab, DynamicConversationTab) and tab.locked_resource:
|
|
||||||
name = safeJID(tab.name)
|
|
||||||
name.resource = tab.locked_resource
|
|
||||||
name = name.full
|
|
||||||
format_dict = {
|
format_dict = {
|
||||||
'jid_c': '\x19%s}' % dump_tuple(get_theme().COLOR_MUC_JID),
|
'jid_c': '\x19%s}' % dump_tuple(get_theme().COLOR_MUC_JID),
|
||||||
'info': '\x19%s}' % dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
|
'info': '\x19%s}' % dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
|
||||||
|
@ -833,11 +847,6 @@ class Plugin(BasePlugin):
|
||||||
if action == 'end': # close the session
|
if action == 'end': # close the session
|
||||||
context = self.get_context(name)
|
context = self.get_context(name)
|
||||||
context.disconnect()
|
context.disconnect()
|
||||||
if isinstance(tab, DynamicConversationTab):
|
|
||||||
ctx = self.find_encrypted_context_with_matching(safeJID(name).bare)
|
|
||||||
while ctx is not None:
|
|
||||||
ctx.disconnect()
|
|
||||||
ctx = self.find_encrypted_context_with_matching(safeJID(name).bare)
|
|
||||||
elif action == 'start' or action == 'refresh':
|
elif action == 'start' or action == 'refresh':
|
||||||
self.otr_start(tab, name, format_dict)
|
self.otr_start(tab, name, format_dict)
|
||||||
elif action == 'ourfpr':
|
elif action == 'ourfpr':
|
||||||
|
@ -891,13 +900,7 @@ class Plugin(BasePlugin):
|
||||||
secs = self.config.get('timeout', 3)
|
secs = self.config.get('timeout', 3)
|
||||||
def notify_otr_timeout():
|
def notify_otr_timeout():
|
||||||
tab_name = tab.name
|
tab_name = tab.name
|
||||||
otr = self.get_context(tab_name)
|
otr = self.find_encrypted_context_with_matching(tab_name)
|
||||||
if isinstance(tab, DynamicConversationTab):
|
|
||||||
if tab.locked_resource:
|
|
||||||
tab_name = safeJID(tab.name)
|
|
||||||
tab_name.resource = tab.locked_resource
|
|
||||||
tab_name = tab_name.full
|
|
||||||
otr = self.get_context(tab_name)
|
|
||||||
if otr.state != STATE_ENCRYPTED:
|
if otr.state != STATE_ENCRYPTED:
|
||||||
format_dict['secs'] = secs
|
format_dict['secs'] = secs
|
||||||
text = OTR_NOT_ENABLED % format_dict
|
text = OTR_NOT_ENABLED % format_dict
|
||||||
|
@ -938,11 +941,6 @@ class Plugin(BasePlugin):
|
||||||
|
|
||||||
tab = self.api.current_tab()
|
tab = self.api.current_tab()
|
||||||
name = tab.name
|
name = tab.name
|
||||||
if isinstance(tab, DynamicConversationTab) and tab.locked_resource:
|
|
||||||
name = safeJID(tab.name)
|
|
||||||
name.resource = tab.locked_resource
|
|
||||||
name = name.full
|
|
||||||
|
|
||||||
format_dict = {
|
format_dict = {
|
||||||
'jid_c': '\x19%s}' % dump_tuple(get_theme().COLOR_MUC_JID),
|
'jid_c': '\x19%s}' % dump_tuple(get_theme().COLOR_MUC_JID),
|
||||||
'info': '\x19%s}' % dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
|
'info': '\x19%s}' % dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
|
||||||
|
@ -983,3 +981,7 @@ def get_tlv(tlvs, cls):
|
||||||
for tlv in tlvs:
|
for tlv in tlvs:
|
||||||
if isinstance(tlv, cls):
|
if isinstance(tlv, cls):
|
||||||
return tlv
|
return tlv
|
||||||
|
|
||||||
|
def is_relevant(tab):
|
||||||
|
"""Check if a tab should be concerned with OTR"""
|
||||||
|
return isinstance(tab, (StaticConversationTab, PrivateTab))
|
||||||
|
|
|
@ -409,6 +409,8 @@ class DynamicConversationTab(ConversationTab):
|
||||||
bad idea so it has been removed.
|
bad idea so it has been removed.
|
||||||
Only one DynamicConversationTab can be opened for a given jid.
|
Only one DynamicConversationTab can be opened for a given jid.
|
||||||
"""
|
"""
|
||||||
|
plugin_commands = {}
|
||||||
|
plugin_keys = {}
|
||||||
|
|
||||||
def __init__(self, core, jid, resource=None):
|
def __init__(self, core, jid, resource=None):
|
||||||
self.locked_resource = None
|
self.locked_resource = None
|
||||||
|
@ -418,6 +420,8 @@ class DynamicConversationTab(ConversationTab):
|
||||||
self.register_command(
|
self.register_command(
|
||||||
'unlock', self.unlock_command, shortdesc='Deprecated, do nothing.')
|
'unlock', self.unlock_command, shortdesc='Deprecated, do nothing.')
|
||||||
self.resize()
|
self.resize()
|
||||||
|
self.update_commands()
|
||||||
|
self.update_keys()
|
||||||
|
|
||||||
def get_info_header(self):
|
def get_info_header(self):
|
||||||
return self.info_header
|
return self.info_header
|
||||||
|
@ -475,12 +479,16 @@ class StaticConversationTab(ConversationTab):
|
||||||
A conversation tab associated with one Full JID. It cannot be locked to
|
A conversation tab associated with one Full JID. It cannot be locked to
|
||||||
an different resource or unlocked.
|
an different resource or unlocked.
|
||||||
"""
|
"""
|
||||||
|
plugin_commands = {}
|
||||||
|
plugin_keys = {}
|
||||||
|
|
||||||
def __init__(self, core, jid):
|
def __init__(self, core, jid):
|
||||||
assert (safeJID(jid).resource)
|
assert (safeJID(jid).resource)
|
||||||
ConversationTab.__init__(self, core, jid)
|
ConversationTab.__init__(self, core, jid)
|
||||||
self.info_header = windows.ConversationInfoWin()
|
self.info_header = windows.ConversationInfoWin()
|
||||||
self.resize()
|
self.resize()
|
||||||
|
self.update_commands()
|
||||||
|
self.update_keys()
|
||||||
|
|
||||||
def get_info_header(self):
|
def get_info_header(self):
|
||||||
return self.info_header
|
return self.info_header
|
||||||
|
|
Loading…
Reference in a new issue