[Brendan Taylor] Gsoc 2007 work : end to end encryptions. Fixes #544
This commit is contained in:
commit
c9a407ca52
|
@ -35,7 +35,7 @@
|
|||
<child internal-child="image">
|
||||
<widget class="GtkImage" id="image1371">
|
||||
<property name="visible">True</property>
|
||||
<property name="stock">gtk-missing-image</property>
|
||||
<property name="stock">gtk-file</property>
|
||||
<property name="icon_size">1</property>
|
||||
</widget>
|
||||
</child>
|
||||
|
@ -49,6 +49,15 @@
|
|||
<signal name="activate" handler="_on_toggle_gpg_menuitem_activate"/>
|
||||
</widget>
|
||||
</child>
|
||||
<child>
|
||||
<widget class="GtkCheckMenuItem" id="toggle_e2e_menuitem">
|
||||
<property name="visible">True</property>
|
||||
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
||||
<property name="label" translatable="yes">Toggle End to End Encryption</property>
|
||||
<property name="use_underline">True</property>
|
||||
<signal name="activate" handler="_on_toggle_e2e_menuitem_activate"/>
|
||||
</widget>
|
||||
</child>
|
||||
<child>
|
||||
<widget class="GtkImageMenuItem" id="add_to_roster_menuitem">
|
||||
<property name="visible">True</property>
|
||||
|
|
|
@ -953,11 +953,11 @@ class ChatControl(ChatControlBase):
|
|||
TYPE_ID = message_control.TYPE_CHAT
|
||||
old_msg_kind = None # last kind of the printed message
|
||||
CHAT_CMDS = ['clear', 'compact', 'help', 'me', 'ping', 'say']
|
||||
|
||||
def __init__(self, parent_win, contact, acct, resource = None):
|
||||
|
||||
def __init__(self, parent_win, contact, acct, session, resource = None):
|
||||
ChatControlBase.__init__(self, self.TYPE_ID, parent_win,
|
||||
'chat_child_vbox', contact, acct, resource)
|
||||
|
||||
|
||||
# for muc use:
|
||||
# widget = self.xml.get_widget('muc_window_actions_button')
|
||||
widget = self.xml.get_widget('message_window_actions_button')
|
||||
|
@ -973,7 +973,7 @@ class ChatControl(ChatControlBase):
|
|||
# it is on enter-notify and leave-notify so no need to be per jid
|
||||
self.show_bigger_avatar_timeout_id = None
|
||||
self.bigger_avatar_window = None
|
||||
self.show_avatar(self.contact.resource)
|
||||
self.show_avatar(self.contact.resource)
|
||||
|
||||
# chatstate timers and state
|
||||
self.reset_kbd_mouse_timeout_vars()
|
||||
|
@ -987,7 +987,7 @@ class ChatControl(ChatControlBase):
|
|||
id = message_tv_buffer.connect('changed',
|
||||
self._on_message_tv_buffer_changed)
|
||||
self.handlers[id] = message_tv_buffer
|
||||
|
||||
|
||||
widget = self.xml.get_widget('avatar_eventbox')
|
||||
id = widget.connect('enter-notify-event',
|
||||
self.on_avatar_eventbox_enter_notify_event)
|
||||
|
@ -1007,7 +1007,12 @@ class ChatControl(ChatControlBase):
|
|||
|
||||
if self.contact.jid in gajim.encrypted_chats[self.account]:
|
||||
self.xml.get_widget('gpg_togglebutton').set_active(True)
|
||||
|
||||
|
||||
self.session = session
|
||||
|
||||
# does this window have an existing, active esession?
|
||||
self.esessioned = False
|
||||
|
||||
self.status_tooltip = gtk.Tooltips()
|
||||
self.update_ui()
|
||||
# restore previous conversation
|
||||
|
@ -1300,13 +1305,13 @@ class ChatControl(ChatControlBase):
|
|||
|
||||
contact = self.contact
|
||||
|
||||
encrypted = bool(self.session) and self.session.enable_encryption
|
||||
|
||||
keyID = ''
|
||||
encrypted = False
|
||||
if self.xml.get_widget('gpg_togglebutton').get_active():
|
||||
keyID = contact.keyID
|
||||
encrypted = True
|
||||
|
||||
|
||||
chatstates_on = gajim.config.get('outgoing_chat_state_notifications') != \
|
||||
'disabled'
|
||||
composing_xep = contact.composing_xep
|
||||
|
@ -1318,7 +1323,7 @@ class ChatControl(ChatControlBase):
|
|||
# this is here (and not in send_chatstate)
|
||||
# because we want it sent with REAL message
|
||||
# (not standlone) eg. one that has body
|
||||
|
||||
|
||||
if contact.our_chatstate:
|
||||
# We already asked for xep 85, don't ask it twice
|
||||
composing_xep = 'asked_once'
|
||||
|
@ -1417,18 +1422,46 @@ class ChatControl(ChatControlBase):
|
|||
kind = 'info'
|
||||
name = ''
|
||||
else:
|
||||
ec = gajim.encrypted_chats[self.account]
|
||||
if encrypted and jid not in ec:
|
||||
msg = _('Encryption enabled')
|
||||
# ESessions
|
||||
if self.session and self.session.enable_encryption:
|
||||
if not self.esessioned:
|
||||
msg = _('Encryption enabled')
|
||||
ChatControlBase.print_conversation_line(self, msg,
|
||||
'status', '', tim)
|
||||
|
||||
if self.session.loggable:
|
||||
msg = _('Session WILL be logged')
|
||||
else:
|
||||
msg = _('Session WILL NOT be logged')
|
||||
|
||||
ChatControlBase.print_conversation_line(self, msg,
|
||||
'status', '', tim)
|
||||
|
||||
self.esessioned = True
|
||||
elif not encrypted:
|
||||
msg = _('The following message was NOT encrypted')
|
||||
ChatControlBase.print_conversation_line(self, msg,
|
||||
'status', '', tim)
|
||||
elif self.esessioned:
|
||||
msg = _('Encryption disabled')
|
||||
ChatControlBase.print_conversation_line(self, msg,
|
||||
'status', '', tim)
|
||||
ec.append(jid)
|
||||
elif not encrypted and jid in ec:
|
||||
msg = _('Encryption disabled')
|
||||
ChatControlBase.print_conversation_line(self, msg,
|
||||
'status', '', tim)
|
||||
ec.remove(jid)
|
||||
self.xml.get_widget('gpg_togglebutton').set_active(encrypted)
|
||||
self.esessioned = False
|
||||
else:
|
||||
# GPG encryption
|
||||
ec = gajim.encrypted_chats[self.account]
|
||||
if encrypted and jid not in ec:
|
||||
msg = _('Encryption enabled')
|
||||
ChatControlBase.print_conversation_line(self, msg,
|
||||
'status', '', tim)
|
||||
ec.append(jid)
|
||||
elif not encrypted and jid in ec:
|
||||
msg = _('Encryption disabled')
|
||||
ChatControlBase.print_conversation_line(self, msg,
|
||||
'status', '', tim)
|
||||
ec.remove(jid)
|
||||
self.xml.get_widget('gpg_togglebutton').set_active(encrypted)
|
||||
|
||||
if not frm:
|
||||
kind = 'incoming'
|
||||
name = contact.get_shown_name()
|
||||
|
@ -1538,6 +1571,7 @@ class ChatControl(ChatControlBase):
|
|||
|
||||
history_menuitem = xml.get_widget('history_menuitem')
|
||||
toggle_gpg_menuitem = xml.get_widget('toggle_gpg_menuitem')
|
||||
toggle_e2e_menuitem = xml.get_widget('toggle_e2e_menuitem')
|
||||
add_to_roster_menuitem = xml.get_widget('add_to_roster_menuitem')
|
||||
send_file_menuitem = xml.get_widget('send_file_menuitem')
|
||||
information_menuitem = xml.get_widget('information_menuitem')
|
||||
|
@ -1557,6 +1591,10 @@ class ChatControl(ChatControlBase):
|
|||
toggle_gpg_menuitem.set_active(isactive)
|
||||
toggle_gpg_menuitem.set_property('sensitive', is_sensitive)
|
||||
|
||||
# TODO: check that the remote client supports e2e
|
||||
isactive = int(self.session != None and self.session.enable_encryption)
|
||||
toggle_e2e_menuitem.set_active(isactive)
|
||||
|
||||
# If we don't have resource, we can't do file transfer
|
||||
# in transports, contact holds our info we need to disable it too
|
||||
if self.TYPE_ID == message_control.TYPE_PM and self.gc_contact.jid and \
|
||||
|
@ -1591,6 +1629,8 @@ class ChatControl(ChatControlBase):
|
|||
self.handlers[id] = add_to_roster_menuitem
|
||||
id = toggle_gpg_menuitem.connect('activate',
|
||||
self._on_toggle_gpg_menuitem_activate)
|
||||
id = toggle_e2e_menuitem.connect('activate',
|
||||
self._on_toggle_e2e_menuitem_activate)
|
||||
self.handlers[id] = toggle_gpg_menuitem
|
||||
id = information_menuitem.connect('activate',
|
||||
self._on_contact_information_menuitem_activate)
|
||||
|
@ -1908,6 +1948,9 @@ class ChatControl(ChatControlBase):
|
|||
encrypted = data[4], subject = data[1], xhtml = data[7])
|
||||
if len(data) > 6 and isinstance(data[6], int):
|
||||
message_ids.append(data[6])
|
||||
|
||||
if len(data) > 8:
|
||||
self.set_session(data[8])
|
||||
if message_ids:
|
||||
gajim.logger.set_read_messages(message_ids)
|
||||
gajim.events.remove_events(self.account, jid_with_resource,
|
||||
|
@ -2035,6 +2078,28 @@ class ChatControl(ChatControlBase):
|
|||
'''user want to invite some friends to chat'''
|
||||
dialogs.TransformChatToMUC(self.account, [self.contact.jid])
|
||||
|
||||
def _on_toggle_e2e_menuitem_activate(self, widget):
|
||||
if self.session and self.session.enable_encryption:
|
||||
self.session.terminate_e2e()
|
||||
|
||||
msg = _('Encryption disabled')
|
||||
ChatControlBase.print_conversation_line(self, msg,
|
||||
'status', '', None)
|
||||
self.esessioned = False
|
||||
|
||||
jid = str(self.session.jid)
|
||||
|
||||
gajim.connections[self.account].delete_session(jid,
|
||||
self.session.thread_id)
|
||||
|
||||
self.session = gajim.connections[self.account].make_new_session(jid)
|
||||
else:
|
||||
if not self.session:
|
||||
self.session = gajim.connections[self.account].make_new_session(
|
||||
self.contact.jid)
|
||||
|
||||
# XXX decide whether to use 4 or 3 message negotiation
|
||||
self.session.negotiate_e2e(False)
|
||||
|
||||
def got_connected(self):
|
||||
ChatControlBase.got_connected(self)
|
||||
|
|
|
@ -122,7 +122,7 @@ def check_and_possibly_create_paths():
|
|||
print _('%s is a directory but should be a file') % LOG_DB_PATH
|
||||
print _('Gajim will now exit')
|
||||
sys.exit()
|
||||
|
||||
|
||||
else: # dot_gajim doesn't exist
|
||||
if dot_gajim: # is '' on win9x so avoid that
|
||||
create_path(dot_gajim)
|
||||
|
|
|
@ -175,6 +175,7 @@ class Config:
|
|||
'tabs_always_visible': [opt_bool, False, _('Show tab when only one conversation?')],
|
||||
'tabs_border': [opt_bool, False, _('Show tabbed notebook border in chat windows?')],
|
||||
'tabs_close_button': [opt_bool, True, _('Show close button in tab?')],
|
||||
'log_encrypted_sessions': [opt_bool, False, _('When negotiating an encrypted session, should Gajim assume you want your messages to be logged?')],
|
||||
'chat_avatar_width': [opt_int, 52],
|
||||
'chat_avatar_height': [opt_int, 52],
|
||||
'roster_avatar_width': [opt_int, 32],
|
||||
|
|
|
@ -105,6 +105,7 @@ class ConfigPaths:
|
|||
def init_profile(self, profile = ''):
|
||||
conffile = windowsify(u'config')
|
||||
pidfile = windowsify(u'gajim')
|
||||
secretsfile = windowsify(u'secrets')
|
||||
|
||||
if len(profile) > 0:
|
||||
conffile += u'.' + profile
|
||||
|
@ -112,6 +113,7 @@ class ConfigPaths:
|
|||
pidfile += u'.pid'
|
||||
self.add_from_root('CONFIG_FILE', conffile)
|
||||
self.add_from_root('PID_FILE', pidfile)
|
||||
self.add_from_root('SECRETS_FILE', secretsfile)
|
||||
|
||||
# for k, v in paths.iteritems():
|
||||
# print "%s: %s" % (repr(k), repr(v))
|
||||
|
|
|
@ -22,6 +22,8 @@ import os
|
|||
import random
|
||||
import socket
|
||||
|
||||
import time
|
||||
|
||||
try:
|
||||
randomsource = random.SystemRandom()
|
||||
except:
|
||||
|
@ -819,7 +821,7 @@ class Connection(ConnectionHandlers):
|
|||
|
||||
def send_message(self, jid, msg, keyID, type = 'chat', subject='',
|
||||
chatstate = None, msg_id = None, composing_xep = None, resource = None,
|
||||
user_nick = None, xhtml = None, forward_from = None):
|
||||
user_nick = None, xhtml = None, session = None, forward_from = None):
|
||||
if not self.connection:
|
||||
return 1
|
||||
if msg and not xhtml and gajim.config.get('rst_formatting_outgoing_messages'):
|
||||
|
@ -868,7 +870,7 @@ class Connection(ConnectionHandlers):
|
|||
msg_iq.setTag('nick', namespace = common.xmpp.NS_NICK).setData(
|
||||
user_nick)
|
||||
|
||||
# chatstates - if peer supports jep85 or jep22, send chatstates
|
||||
# chatstates - if peer supports xep85 or xep22, send chatstates
|
||||
# please note that the only valid tag inside a message containing a <body>
|
||||
# tag is the active event
|
||||
if chatstate is not None:
|
||||
|
@ -893,6 +895,15 @@ class Connection(ConnectionHandlers):
|
|||
namespace=common.xmpp.NS_ADDRESS)
|
||||
addresses.addChild('address', attrs = {'type': 'ofrom',
|
||||
'jid': forward_from})
|
||||
if session:
|
||||
# XEP-0201
|
||||
session.last_send = time.time()
|
||||
msg_iq.setThread(session.thread_id)
|
||||
|
||||
# XEP-0200
|
||||
if session.enable_encryption:
|
||||
msg_iq = session.encrypt_stanza(msg_iq)
|
||||
|
||||
self.connection.send(msg_iq)
|
||||
if not forward_from:
|
||||
no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for')\
|
||||
|
@ -912,6 +923,18 @@ class Connection(ConnectionHandlers):
|
|||
except exceptions.PysqliteOperationalError, e:
|
||||
self.dispatch('ERROR', (_('Disk Write Error'), str(e)))
|
||||
self.dispatch('MSGSENT', (jid, msg, keyID))
|
||||
|
||||
if session.is_loggable():
|
||||
log_msg = msg
|
||||
if subject:
|
||||
log_msg = _('Subject: %s\n%s') % (subject, msg)
|
||||
if log_msg:
|
||||
if type == 'chat':
|
||||
kind = 'chat_msg_sent'
|
||||
else:
|
||||
kind = 'single_msg_sent'
|
||||
gajim.logger.write(kind, jid, log_msg)
|
||||
self.dispatch('MSGSENT', (jid, msg, keyID))
|
||||
|
||||
def send_stanza(self, stanza):
|
||||
''' send a stanza untouched '''
|
||||
|
|
|
@ -24,7 +24,7 @@ import socket
|
|||
import sys
|
||||
|
||||
from time import (altzone, daylight, gmtime, localtime, mktime, strftime,
|
||||
time as time_time, timezone, tzname)
|
||||
time as time_time, timezone, tzname)
|
||||
from calendar import timegm
|
||||
|
||||
import socks5
|
||||
|
@ -39,6 +39,8 @@ from common.commands import ConnectionCommands
|
|||
from common.pubsub import ConnectionPubSub
|
||||
from common.caps import ConnectionCaps
|
||||
|
||||
from common.stanza_session import EncryptedStanzaSession
|
||||
|
||||
STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd',
|
||||
'invisible', 'error']
|
||||
# kind of events we can wait for an answer
|
||||
|
@ -57,7 +59,7 @@ except:
|
|||
class ConnectionBytestream:
|
||||
def __init__(self):
|
||||
self.files_props = {}
|
||||
|
||||
|
||||
def is_transfer_stoped(self, file_props):
|
||||
if file_props.has_key('error') and file_props['error'] != 0:
|
||||
return True
|
||||
|
@ -346,7 +348,7 @@ class ConnectionBytestream:
|
|||
iq.setID(auth_id)
|
||||
query = iq.setTag('query')
|
||||
query.setNamespace(common.xmpp.NS_BYTESTREAM)
|
||||
query.setAttr('sid', proxy['sid'])
|
||||
query.setAttr('sid', proxy['sid'])
|
||||
activate = query.setTag('activate')
|
||||
activate.setData(file_props['proxy_receiver'])
|
||||
iq.setID(auth_id)
|
||||
|
@ -437,7 +439,7 @@ class ConnectionBytestream:
|
|||
gajim.proxy65_manager.resolve_result(frm, query)
|
||||
|
||||
try:
|
||||
streamhost = query.getTag('streamhost-used')
|
||||
streamhost = query.getTag('streamhost-used')
|
||||
except: # this bytestream result is not what we need
|
||||
pass
|
||||
id = real_id[3:]
|
||||
|
@ -703,7 +705,7 @@ class ConnectionDisco:
|
|||
def _DiscoverItemsGetCB(self, con, iq_obj):
|
||||
gajim.log.debug('DiscoverItemsGetCB')
|
||||
node = iq_obj.getTagAttr('query', 'node')
|
||||
if node is None:
|
||||
if node is None:
|
||||
result = iq_obj.buildReply('result')
|
||||
self.connection.send(result)
|
||||
raise common.xmpp.NodeProcessed
|
||||
|
@ -738,6 +740,7 @@ class ConnectionDisco:
|
|||
q.addChild('feature', attrs = {'var': common.xmpp.NS_MUC})
|
||||
q.addChild('feature', attrs = {'var': common.xmpp.NS_COMMANDS})
|
||||
q.addChild('feature', attrs = {'var': common.xmpp.NS_DISCO_INFO})
|
||||
q.addChild('feature', attrs = {'var': common.xmpp.NS_ESESSION_INIT})
|
||||
|
||||
if (node is None or extension == 'cstates') and gajim.config.get('outgoing_chat_state_notifactions') != 'disabled':
|
||||
q.addChild('feature', attrs = {'var': common.xmpp.NS_CHATSTATES})
|
||||
|
@ -779,12 +782,12 @@ class ConnectionDisco:
|
|||
for key in i.getAttrs().keys():
|
||||
attr[key] = i.getAttr(key)
|
||||
if attr.has_key('category') and \
|
||||
attr['category'] in ('gateway', 'headline') and \
|
||||
attr.has_key('type'):
|
||||
attr['category'] in ('gateway', 'headline') and \
|
||||
attr.has_key('type'):
|
||||
transport_type = attr['type']
|
||||
if attr.has_key('category') and \
|
||||
attr['category'] == 'conference' and \
|
||||
attr.has_key('type') and attr['type'] == 'text':
|
||||
attr['category'] == 'conference' and \
|
||||
attr.has_key('type') and attr['type'] == 'text':
|
||||
is_muc = True
|
||||
identities.append(attr)
|
||||
elif i.getName() == 'feature':
|
||||
|
@ -1193,14 +1196,17 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
|
|||
# keep the jids we auto added (transports contacts) to not send the
|
||||
# SUBSCRIBED event to gui
|
||||
self.automatically_added = []
|
||||
# keep the latest subscribed event for each jid to prevent loop when we
|
||||
# keep the latest subscribed event for each jid to prevent loop when we
|
||||
# acknoledge presences
|
||||
self.subscribed_events = {}
|
||||
|
||||
# keep track of sessions this connection has with other JIDs
|
||||
self.sessions = {}
|
||||
try:
|
||||
idle.init()
|
||||
except:
|
||||
HAS_IDLE = False
|
||||
|
||||
|
||||
def build_http_auth_answer(self, iq_obj, answer):
|
||||
if answer == 'yes':
|
||||
self.connection.send(iq_obj.buildReply('result'))
|
||||
|
@ -1222,6 +1228,29 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
|
|||
self.dispatch('HTTP_AUTH', (method, url, id, iq_obj, msg));
|
||||
raise common.xmpp.NodeProcessed
|
||||
|
||||
def _FeatureNegCB(self, con, stanza, session):
|
||||
gajim.log.debug('FeatureNegCB')
|
||||
feature = stanza.getTag(name='feature', namespace=common.xmpp.NS_FEATURE)
|
||||
form = common.xmpp.DataForm(node=feature.getTag('x'))
|
||||
|
||||
if form['FORM_TYPE'] == 'urn:xmpp:ssn':
|
||||
self.dispatch('SESSION_NEG', (stanza.getFrom(), session, form))
|
||||
else:
|
||||
reply = stanza.buildReply()
|
||||
reply.setType('error')
|
||||
|
||||
reply.addChild(feature)
|
||||
reply.addChild(node=xmpp.ErrorNode('service-unavailable', typ='cancel'))
|
||||
|
||||
con.send(reply)
|
||||
|
||||
def _InitE2ECB(self, con, stanza, session):
|
||||
gajim.log.debug('InitE2ECB')
|
||||
init = stanza.getTag(name='init', namespace=common.xmpp.NS_ESESSION_INIT)
|
||||
form = common.xmpp.DataForm(node=init.getTag('x'))
|
||||
|
||||
self.dispatch('SESSION_NEG', (stanza.getFrom(), session, form))
|
||||
|
||||
def _ErrorCB(self, con, iq_obj):
|
||||
gajim.log.debug('ErrorCB')
|
||||
if iq_obj.getQueryNS() == common.xmpp.NS_VERSION:
|
||||
|
@ -1298,10 +1327,10 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
|
|||
def _rosterSetCB(self, con, iq_obj):
|
||||
gajim.log.debug('rosterSetCB')
|
||||
for item in iq_obj.getTag('query').getChildren():
|
||||
jid = helpers.parse_jid(item.getAttr('jid'))
|
||||
jid = helpers.parse_jid(item.getAttr('jid'))
|
||||
name = item.getAttr('name')
|
||||
sub = item.getAttr('subscription')
|
||||
ask = item.getAttr('ask')
|
||||
sub = item.getAttr('subscription')
|
||||
ask = item.getAttr('ask')
|
||||
groups = []
|
||||
for group in item.getTags('group'):
|
||||
groups.append(group.getData())
|
||||
|
@ -1384,7 +1413,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
|
|||
gajim.log.debug('TimeRevisedCB')
|
||||
iq_obj = iq_obj.buildReply('result')
|
||||
qp = iq_obj.setTag('time',
|
||||
namespace=common.xmpp.NS_TIME_REVISED)
|
||||
namespace=common.xmpp.NS_TIME_REVISED)
|
||||
qp.setTagData('utc', strftime('%Y-%m-%dT%TZ', gmtime()))
|
||||
zone = -(timezone, altzone)[daylight] / 60
|
||||
tzo = (zone / 60, abs(zone % 60))
|
||||
|
@ -1437,6 +1466,19 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
|
|||
|
||||
def _messageCB(self, con, msg):
|
||||
'''Called when we receive a message'''
|
||||
frm = helpers.get_full_jid_from_iq(msg)
|
||||
mtype = msg.getType()
|
||||
thread_id = msg.getThread()
|
||||
|
||||
if not mtype:
|
||||
mtype = 'normal'
|
||||
|
||||
if not mtype == 'groupchat':
|
||||
session = self.get_session(frm, thread_id, mtype)
|
||||
|
||||
if thread_id and not session.received_thread_id:
|
||||
session.received_thread_id = True
|
||||
|
||||
# check if the message is pubsub#event
|
||||
if msg.getTag('event') is not None:
|
||||
self._pubsubEventCB(con, msg)
|
||||
|
@ -1446,9 +1488,30 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
|
|||
common.xmpp.NS_HTTP_AUTH:
|
||||
self._HttpAuthCB(con, msg)
|
||||
return
|
||||
if msg.getTag('feature') and msg.getTag('feature').namespace == \
|
||||
common.xmpp.NS_FEATURE:
|
||||
self._FeatureNegCB(con, msg, session)
|
||||
return
|
||||
if msg.getTag('init') and msg.getTag('init').namespace == \
|
||||
common.xmpp.NS_ESESSION_INIT:
|
||||
self._InitE2ECB(con, msg, session)
|
||||
|
||||
encrypted = False
|
||||
tim = msg.getTimestamp()
|
||||
tim = strptime(tim, '%Y%m%dT%H:%M:%S')
|
||||
tim = localtime(timegm(tim))
|
||||
|
||||
e2e_tag = msg.getTag('c', namespace = common.xmpp.NS_STANZA_CRYPTO)
|
||||
if e2e_tag:
|
||||
encrypted = True
|
||||
|
||||
try:
|
||||
msg = session.decrypt_stanza(msg)
|
||||
except:
|
||||
self.dispatch('FAILED_DECRYPT', (frm, tim))
|
||||
|
||||
msgtxt = msg.getBody()
|
||||
msghtml = msg.getXHTML()
|
||||
mtype = msg.getType()
|
||||
subject = msg.getSubject() # if not there, it's None
|
||||
tim = msg.getTimestamp()
|
||||
tim = helpers.datetime_tuple(tim)
|
||||
|
@ -1518,7 +1581,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
|
|||
if encTag and GnuPG.USE_GPG:
|
||||
#decrypt
|
||||
encmsg = encTag.getData()
|
||||
|
||||
|
||||
keyID = gajim.config.get_per('accounts', self.name, 'keyid')
|
||||
if keyID:
|
||||
decmsg = self.gpg.decrypt(encmsg, keyID)
|
||||
|
@ -1530,7 +1593,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
|
|||
if not error_msg:
|
||||
error_msg = msgtxt
|
||||
msgtxt = None
|
||||
if self.name not in no_log_for:
|
||||
if session.is_loggable():
|
||||
try:
|
||||
gajim.logger.write('error', frm, error_msg, tim = tim,
|
||||
subject = subject)
|
||||
|
@ -1568,8 +1631,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
|
|||
elif mtype == 'chat': # it's type 'chat'
|
||||
if not msg.getTag('body') and chatstate is None: #no <body>
|
||||
return
|
||||
if msg.getTag('body') and self.name not in no_log_for and jid not in\
|
||||
no_log_for and msgtxt:
|
||||
if msg.getTag('body') and session.is_loggable() and msgtxt:
|
||||
try:
|
||||
msg_id = gajim.logger.write('chat_msg_recv', frm, msgtxt,
|
||||
tim = tim, subject = subject)
|
||||
|
@ -1588,7 +1650,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
|
|||
self.dispatch('GC_INVITATION',(frm, jid_from, reason, password,
|
||||
is_continued))
|
||||
return
|
||||
if self.name not in no_log_for and jid not in no_log_for and msgtxt:
|
||||
if session.is_loggable()and msgtxt:
|
||||
try:
|
||||
gajim.logger.write('single_msg_recv', frm, msgtxt, tim = tim,
|
||||
subject = subject)
|
||||
|
@ -1599,9 +1661,81 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
|
|||
if treat_as:
|
||||
mtype = treat_as
|
||||
self.dispatch('MSG', (frm, msgtxt, tim, encrypted, mtype,
|
||||
subject, chatstate, msg_id, composing_xep, user_nick, msghtml))
|
||||
subject, chatstate, msg_id, composing_xep, user_nick, msghtml,
|
||||
session))
|
||||
# END messageCB
|
||||
|
||||
def get_session(self, jid, thread_id, type):
|
||||
'''returns an existing session between this connection and 'jid', returns a new one if none exist.'''
|
||||
session = self.find_session(jid, thread_id, type)
|
||||
|
||||
if session:
|
||||
return session
|
||||
else:
|
||||
# it's possible we initiated a session with a bare JID and this is the
|
||||
# first time we've seen a resource
|
||||
bare_jid = gajim.get_jid_without_resource(jid)
|
||||
if bare_jid != jid:
|
||||
session = self.find_session(bare_jid, thread_id, type)
|
||||
if session:
|
||||
if not session.received_thread_id:
|
||||
thread_id = session.thread_id
|
||||
|
||||
self.move_session(bare_jid, thread_id, jid.split("/")[1])
|
||||
return session
|
||||
|
||||
return self.make_new_session(jid, thread_id, type)
|
||||
|
||||
def find_session(self, jid, thread_id, type):
|
||||
try:
|
||||
if type == 'chat' and not thread_id:
|
||||
return self.find_null_session(jid)
|
||||
else:
|
||||
return self.sessions[jid][thread_id]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def delete_session(self, jid, thread_id):
|
||||
del self.sessions[jid][thread_id]
|
||||
|
||||
if not self.sessions[jid]:
|
||||
del self.sessions[jid]
|
||||
|
||||
def move_session(self, original_jid, thread_id, to_resource):
|
||||
'''moves a session to another resource.'''
|
||||
session = self.sessions[original_jid][thread_id]
|
||||
|
||||
del self.sessions[original_jid][thread_id]
|
||||
|
||||
new_jid = gajim.get_jid_without_resource(original_jid) + '/' + to_resource
|
||||
session.jid = common.xmpp.JID(new_jid)
|
||||
|
||||
if not new_jid in self.sessions:
|
||||
self.sessions[new_jid] = {}
|
||||
|
||||
self.sessions[new_jid][thread_id] = session
|
||||
|
||||
def find_null_session(self, jid):
|
||||
'''finds all of the sessions between us and jid that jid hasn't sent a thread_id in yet.
|
||||
|
||||
returns the session that we last sent a message to.'''
|
||||
|
||||
sessions_with_jid = self.sessions[jid].values()
|
||||
no_threadid_sessions = filter(lambda s: not s.received_thread_id, sessions_with_jid)
|
||||
no_threadid_sessions.sort(key=lambda s: s.last_send)
|
||||
|
||||
return no_threadid_sessions[-1]
|
||||
|
||||
def make_new_session(self, jid, thread_id = None, type = 'chat'):
|
||||
sess = EncryptedStanzaSession(self, common.xmpp.JID(jid), thread_id, type)
|
||||
|
||||
if not jid in self.sessions:
|
||||
self.sessions[jid] = {}
|
||||
|
||||
self.sessions[jid][sess.thread_id] = sess
|
||||
|
||||
return sess
|
||||
|
||||
def _pubsubEventCB(self, con, msg):
|
||||
''' Called when we receive <message/> with pubsub event. '''
|
||||
# TODO: Logging? (actually services where logging would be useful, should
|
||||
|
|
|
@ -0,0 +1,207 @@
|
|||
import string
|
||||
|
||||
# This file defines a number of constants; specifically, large primes suitable for
|
||||
# use with the Diffie-Hellman key exchange.
|
||||
#
|
||||
# These constants have been obtained from RFC2409 and RFC3526.
|
||||
|
||||
generators = [ None, # one to get the right offset
|
||||
2,
|
||||
2,
|
||||
None,
|
||||
None,
|
||||
2,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
2, # group 14
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
]
|
||||
|
||||
hex_primes = [ None,
|
||||
|
||||
# group 1
|
||||
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
|
||||
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
|
||||
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
|
||||
E485B576 625E7EC6 F44C42E9 A63A3620 FFFFFFFF FFFFFFFF''',
|
||||
|
||||
# group 2
|
||||
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
|
||||
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
|
||||
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
|
||||
E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
|
||||
EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE65381
|
||||
FFFFFFFF FFFFFFFF''',
|
||||
|
||||
# XXX how do I obtain these?
|
||||
None,
|
||||
None,
|
||||
|
||||
# group 5
|
||||
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
|
||||
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
|
||||
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
|
||||
E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
|
||||
EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D
|
||||
C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F
|
||||
83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D
|
||||
670C354E 4ABC9804 F1746C08 CA237327 FFFFFFFF FFFFFFFF''',
|
||||
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
|
||||
# group 14
|
||||
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
|
||||
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
|
||||
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
|
||||
E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
|
||||
EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D
|
||||
C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F
|
||||
83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D
|
||||
670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B
|
||||
E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9
|
||||
DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510
|
||||
15728E5A 8AACAA68 FFFFFFFF FFFFFFFF''',
|
||||
|
||||
# group 15
|
||||
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
|
||||
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
|
||||
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
|
||||
E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
|
||||
EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D
|
||||
C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F
|
||||
83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D
|
||||
670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B
|
||||
E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9
|
||||
DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510
|
||||
15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64
|
||||
ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7
|
||||
ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B
|
||||
F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C
|
||||
BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31
|
||||
43DB5BFC E0FD108E 4B82D120 A93AD2CA FFFFFFFF FFFFFFFF''',
|
||||
|
||||
# group 16
|
||||
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
|
||||
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
|
||||
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
|
||||
E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
|
||||
EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D
|
||||
C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F
|
||||
83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D
|
||||
670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B
|
||||
E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9
|
||||
DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510
|
||||
15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64
|
||||
ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7
|
||||
ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B
|
||||
F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C
|
||||
BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31
|
||||
43DB5BFC E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7
|
||||
88719A10 BDBA5B26 99C32718 6AF4E23C 1A946834 B6150BDA
|
||||
2583E9CA 2AD44CE8 DBBBC2DB 04DE8EF9 2E8EFC14 1FBECAA6
|
||||
287C5947 4E6BC05D 99B2964F A090C3A2 233BA186 515BE7ED
|
||||
1F612970 CEE2D7AF B81BDD76 2170481C D0069127 D5B05AA9
|
||||
93B4EA98 8D8FDDC1 86FFFB7DC 90A6C08F 4DF435C9 34063199
|
||||
FFFFFFFF FFFFFFFF''',
|
||||
|
||||
# group 17
|
||||
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08
|
||||
8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B
|
||||
302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9
|
||||
A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6
|
||||
49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8
|
||||
FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D
|
||||
670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C
|
||||
180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718
|
||||
3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D
|
||||
04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D
|
||||
B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226
|
||||
1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C
|
||||
BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC
|
||||
E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26
|
||||
99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB
|
||||
04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2
|
||||
233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127
|
||||
D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492
|
||||
36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD F8FF9406
|
||||
AD9E530E E5DB382F 413001AE B06A53ED 9027D831 179727B0 865A8918
|
||||
DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B DB7F1447 E6CC254B 33205151
|
||||
2BD7AF42 6FB8F401 378CD2BF 5983CA01 C64B92EC F032EA15 D1721D03
|
||||
F482D7CE 6E74FEF6 D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F
|
||||
BEC7E8F3 23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA
|
||||
CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328 06A1D58B
|
||||
B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C DA56C9EC 2EF29632
|
||||
387FE8D7 6E3C0468 043E8F66 3F4860EE 12BF2D5B 0B7474D6 E694F91E
|
||||
6DCC4024 FFFFFFFF FFFFFFFF''',
|
||||
|
||||
# group 18
|
||||
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
|
||||
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
|
||||
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
|
||||
E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
|
||||
EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D
|
||||
C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F
|
||||
83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D
|
||||
670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B
|
||||
E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9
|
||||
DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510
|
||||
15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64
|
||||
ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7
|
||||
ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B
|
||||
F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C
|
||||
BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31
|
||||
43DB5BFC E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7
|
||||
88719A10 BDBA5B26 99C32718 6AF4E23C 1A946834 B6150BDA
|
||||
2583E9CA 2AD44CE8 DBBBC2DB 04DE8EF9 2E8EFC14 1FBECAA6
|
||||
287C5947 4E6BC05D 99B2964F A090C3A2 233BA186 515BE7ED
|
||||
1F612970 CEE2D7AF B81BDD76 2170481C D0069127 D5B05AA9
|
||||
93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492
|
||||
36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD
|
||||
F8FF9406 AD9E530E E5DB382F 413001AE B06A53ED 9027D831
|
||||
179727B0 865A8918 DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B
|
||||
DB7F1447 E6CC254B 33205151 2BD7AF42 6FB8F401 378CD2BF
|
||||
5983CA01 C64B92EC F032EA15 D1721D03 F482D7CE 6E74FEF6
|
||||
D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F BEC7E8F3
|
||||
23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA
|
||||
CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328
|
||||
06A1D58B B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C
|
||||
DA56C9EC 2EF29632 387FE8D7 6E3C0468 043E8F66 3F4860EE
|
||||
12BF2D5B 0B7474D6 E694F91E 6DBE1159 74A3926F 12FEE5E4
|
||||
38777CB6 A932DF8C D8BEC4D0 73B931BA 3BC832B6 8D9DD300
|
||||
741FA7BF 8AFC47ED 2576F693 6BA42466 3AAB639C 5AE4F568
|
||||
3423B474 2BF1C978 238F16CB E39D652D E3FDB8BE FC848AD9
|
||||
22222E04 A4037C07 13EB57A8 1A23F0C7 3473FC64 6CEA306B
|
||||
4BCBC886 2F8385DD FA9D4B7F A2C087E8 79683303 ED5BDD3A
|
||||
062B3CF5 B3A278A6 6D2A13F8 3F44F82D DF310EE0 74AB6A36
|
||||
4597E899 A0255DC1 64F31CC5 0846851D F9AB4819 5DED7EA1
|
||||
B1D510BD 7EE74D73 FAF36BC3 1ECFA268 359046F4 EB879F92
|
||||
4009438B 481C6CD7 889A002E D5EE382B C9190DA6 FC026E47
|
||||
9558E447 5677E9AA 9E3050E2 765694DF C81F56E8 80B96E71
|
||||
60C980DD 98EDD3DF FFFFFFFF FFFFFFFF'''
|
||||
]
|
||||
|
||||
all_ascii = ''.join(map(chr, range(256)))
|
||||
|
||||
def hex_to_decimal(stripee):
|
||||
if not stripee:
|
||||
return None
|
||||
|
||||
return int(stripee.translate(all_ascii, string.whitespace), 16)
|
||||
|
||||
primes = map(hex_to_decimal, hex_primes)
|
|
@ -54,8 +54,16 @@ class SessionBusNotPresent(Exception):
|
|||
def __str__(self):
|
||||
return _('Session bus is not available.\nTry reading http://trac.gajim.org/wiki/GajimDBus')
|
||||
|
||||
class NegotiationError(Exception):
|
||||
'''A session negotiation failed'''
|
||||
pass
|
||||
|
||||
class DecryptionError(Exception):
|
||||
'''A message couldn't be decrypted into usable XML'''
|
||||
pass
|
||||
|
||||
class GajimGeneralException(Exception):
|
||||
'''This exception ir our general exception'''
|
||||
'''This exception is our general exception'''
|
||||
def __init__(self, text=''):
|
||||
Exception.__init__(self)
|
||||
self.text = text
|
||||
|
|
|
@ -0,0 +1,927 @@
|
|||
from common import gajim
|
||||
|
||||
from common import xmpp
|
||||
from common import helpers
|
||||
from common import exceptions
|
||||
|
||||
import random
|
||||
import string
|
||||
|
||||
import math
|
||||
import os
|
||||
import time
|
||||
|
||||
from common import dh
|
||||
import xmpp.c14n
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Hash import HMAC, SHA256
|
||||
from Crypto.PublicKey import RSA
|
||||
|
||||
import base64
|
||||
|
||||
XmlDsig = 'http://www.w3.org/2000/09/xmldsig#'
|
||||
|
||||
class StanzaSession(object):
|
||||
def __init__(self, conn, jid, thread_id, type):
|
||||
self.conn = conn
|
||||
|
||||
self.jid = jid
|
||||
|
||||
self.type = type
|
||||
|
||||
if thread_id:
|
||||
self.received_thread_id = True
|
||||
self.thread_id = thread_id
|
||||
else:
|
||||
self.received_thread_id = False
|
||||
if type == 'normal':
|
||||
self.thread_id = None
|
||||
else:
|
||||
self.thread_id = self.generate_thread_id()
|
||||
|
||||
self.last_send = 0
|
||||
self.status = None
|
||||
self.negotiated = {}
|
||||
|
||||
def generate_thread_id(self):
|
||||
return "".join([random.choice(string.letters) for x in xrange(0,32)])
|
||||
|
||||
def send(self, msg):
|
||||
if self.thread_id:
|
||||
msg.NT.thread = self.thread_id
|
||||
|
||||
msg.setAttr('to', self.jid)
|
||||
self.conn.send_stanza(msg)
|
||||
|
||||
if isinstance(msg, xmpp.Message):
|
||||
self.last_send = time.time()
|
||||
|
||||
def reject_negotiation(self, body = None):
|
||||
msg = xmpp.Message()
|
||||
feature = msg.NT.feature
|
||||
feature.setNamespace(xmpp.NS_FEATURE)
|
||||
|
||||
x = xmpp.DataForm(typ='submit')
|
||||
x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn'))
|
||||
x.addChild(node=xmpp.DataField(name='accept', value='0'))
|
||||
|
||||
feature.addChild(node=x)
|
||||
|
||||
if body:
|
||||
msg.setBody(body)
|
||||
|
||||
self.send(msg)
|
||||
|
||||
self.cancelled_negotiation()
|
||||
|
||||
def cancelled_negotiation(self):
|
||||
'''A negotiation has been cancelled, so reset this session to its default state.'''
|
||||
self.status = None
|
||||
self.negotiated = {}
|
||||
|
||||
def terminate(self):
|
||||
msg = xmpp.Message()
|
||||
feature = msg.NT.feature
|
||||
feature.setNamespace(xmpp.NS_FEATURE)
|
||||
|
||||
x = xmpp.DataForm(typ='submit')
|
||||
x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn'))
|
||||
x.addChild(node=xmpp.DataField(name='terminate', value='1'))
|
||||
|
||||
feature.addChild(node=x)
|
||||
|
||||
self.send(msg)
|
||||
|
||||
self.status = None
|
||||
|
||||
def acknowledge_termination(self):
|
||||
# we could send an acknowledgement message here, but we won't.
|
||||
self.status = None
|
||||
|
||||
# an encrypted stanza negotiation has several states. i've represented them as the following values in the 'status'
|
||||
# attribute of the session object:
|
||||
|
||||
# 1. None:
|
||||
# default state
|
||||
# 2. 'requested-e2e':
|
||||
# this client has initiated an esession negotiation and is waiting for
|
||||
# a response
|
||||
# 3. 'responded-e2e':
|
||||
# this client has responded to an esession negotiation request and is
|
||||
# waiting for the initiator to identify itself and complete the
|
||||
# negotiation
|
||||
# 4. 'identified-alice':
|
||||
# this client identified itself and is waiting for the responder to
|
||||
# identify itself and complete the negotiation
|
||||
# 5. 'active':
|
||||
# an encrypted session has been successfully negotiated. messages of
|
||||
# any of the types listed in 'encryptable_stanzas' should be encrypted
|
||||
# before they're sent.
|
||||
|
||||
# the transition between these states is handled in gajim.py's
|
||||
# handle_session_negotiation method.
|
||||
|
||||
class EncryptedStanzaSession(StanzaSession):
|
||||
def __init__(self, conn, jid, thread_id, type = 'chat'):
|
||||
StanzaSession.__init__(self, conn, jid, thread_id, type = 'chat')
|
||||
|
||||
self.loggable = True
|
||||
|
||||
self.xes = {}
|
||||
self.es = {}
|
||||
|
||||
self.n = 128
|
||||
|
||||
self.enable_encryption = False
|
||||
|
||||
# _s denotes 'self' (ie. this client)
|
||||
self._kc_s = None
|
||||
|
||||
# _o denotes 'other' (ie. the client at the other end of the session)
|
||||
self._kc_o = None
|
||||
|
||||
# keep the encrypter updated with my latest cipher key
|
||||
def set_kc_s(self, value):
|
||||
self._kc_s = value
|
||||
self.encrypter = self.cipher.new(self._kc_s, self.cipher.MODE_CTR, counter=self.encryptcounter)
|
||||
|
||||
def get_kc_s(self):
|
||||
return self._kc_s
|
||||
|
||||
# keep the decrypter updated with the other party's latest cipher key
|
||||
def set_kc_o(self, value):
|
||||
self._kc_o = value
|
||||
self.decrypter = self.cipher.new(self._kc_o, self.cipher.MODE_CTR, counter=self.decryptcounter)
|
||||
|
||||
def get_kc_o(self):
|
||||
return self._kc_o
|
||||
|
||||
kc_s = property(get_kc_s, set_kc_s)
|
||||
kc_o = property(get_kc_o, set_kc_o)
|
||||
|
||||
# convert a large integer to a big-endian bitstring
|
||||
def encode_mpi(self, n):
|
||||
if n >= 256:
|
||||
return self.encode_mpi(n / 256) + chr(n % 256)
|
||||
else:
|
||||
return chr(n)
|
||||
|
||||
# convert a large integer to a big-endian bitstring, padded with \x00s to 16 bytes
|
||||
def encode_mpi_with_padding(self, n):
|
||||
ret = self.encode_mpi(n)
|
||||
|
||||
mod = len(ret) % 16
|
||||
if mod != 0:
|
||||
ret = ((16 - mod) * '\x00') + ret
|
||||
|
||||
return ret
|
||||
|
||||
# convert a big-endian bitstring to an integer
|
||||
def decode_mpi(self, s):
|
||||
if len(s) == 0:
|
||||
return 0
|
||||
else:
|
||||
return 256 * self.decode_mpi(s[:-1]) + ord(s[-1])
|
||||
|
||||
def encryptcounter(self):
|
||||
self.c_s = (self.c_s + 1) % (2 ** self.n)
|
||||
return self.encode_mpi_with_padding(self.c_s)
|
||||
|
||||
def decryptcounter(self):
|
||||
self.c_o = (self.c_o + 1) % (2 ** self.n)
|
||||
return self.encode_mpi_with_padding(self.c_o)
|
||||
|
||||
def sign(self, string):
|
||||
if self.negotiated['sign_algs'] == (XmlDsig + 'rsa-sha256'):
|
||||
hash = self.sha256(string)
|
||||
return self.encode_mpi(gajim.interface.pubkey.sign(hash, '')[0])
|
||||
|
||||
def encrypt_stanza(self, stanza):
|
||||
encryptable = filter(lambda x: x.getName() not in ('error', 'amp', 'thread'), stanza.getChildren())
|
||||
|
||||
# XXX can also encrypt contents of <error/> elements in stanzas @type = 'error'
|
||||
# (except for <defined-condition xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/> child elements)
|
||||
|
||||
old_en_counter = self.c_s
|
||||
|
||||
for element in encryptable:
|
||||
stanza.delChild(element)
|
||||
|
||||
plaintext = ''.join(map(str, encryptable))
|
||||
|
||||
m_compressed = self.compress(plaintext)
|
||||
m_final = self.encrypt(m_compressed)
|
||||
|
||||
c = stanza.NT.c
|
||||
c.setNamespace('http://www.xmpp.org/extensions/xep-0200.html#ns')
|
||||
c.NT.data = base64.b64encode(m_final)
|
||||
|
||||
# XXX check for rekey request, handle <key/> elements
|
||||
|
||||
m_content = ''.join(map(str, c.getChildren()))
|
||||
c.NT.mac = base64.b64encode(self.hmac(self.km_s, m_content + self.encode_mpi(old_en_counter)))
|
||||
|
||||
return stanza
|
||||
|
||||
def hmac(self, key, content):
|
||||
return HMAC.new(key, content, self.hash_alg).digest()
|
||||
|
||||
def sha256(self, string):
|
||||
sh = SHA256.new()
|
||||
sh.update(string)
|
||||
return sh.digest()
|
||||
|
||||
base28_chr = "acdefghikmopqruvwxy123456789"
|
||||
|
||||
def sas_28x5(self, m_a, form_b):
|
||||
sha = self.sha256(m_a + form_b + 'Short Authentication String')
|
||||
lsb24 = self.decode_mpi(sha[-3:])
|
||||
return self.base28(lsb24)
|
||||
|
||||
def base28(self, n):
|
||||
if n >= 28:
|
||||
return self.base28(n / 28) + self.base28_chr[n % 28]
|
||||
else:
|
||||
return self.base28_chr[n]
|
||||
|
||||
def generate_initiator_keys(self, k):
|
||||
return (self.hmac(k, 'Initiator Cipher Key'),
|
||||
self.hmac(k, 'Initiator MAC Key'),
|
||||
self.hmac(k, 'Initiator SIGMA Key') )
|
||||
|
||||
def generate_responder_keys(self, k):
|
||||
return (self.hmac(k, 'Responder Cipher Key'),
|
||||
self.hmac(k, 'Responder MAC Key'),
|
||||
self.hmac(k, 'Responder SIGMA Key') )
|
||||
|
||||
def compress(self, plaintext):
|
||||
if self.compression == None:
|
||||
return plaintext
|
||||
|
||||
def decompress(self, compressed):
|
||||
if self.compression == None:
|
||||
return compressed
|
||||
|
||||
def encrypt(self, encryptable):
|
||||
len_padding = 16 - (len(encryptable) % 16)
|
||||
if len_padding != 16:
|
||||
encryptable += len_padding * ' '
|
||||
|
||||
return self.encrypter.encrypt(encryptable)
|
||||
|
||||
# FIXME: use a real PRNG
|
||||
def random_bytes(self, bytes):
|
||||
return os.urandom(bytes)
|
||||
|
||||
def generate_nonce(self):
|
||||
return self.random_bytes(8)
|
||||
|
||||
def decrypt_stanza(self, stanza):
|
||||
c = stanza.getTag(name='c', namespace='http://www.xmpp.org/extensions/xep-0200.html#ns')
|
||||
|
||||
stanza.delChild(c)
|
||||
|
||||
# contents of <c>, minus <mac>, minus whitespace
|
||||
macable = ''.join(map(str, filter(lambda x: x.getName() != 'mac', c.getChildren())))
|
||||
|
||||
received_mac = base64.b64decode(c.getTagData('mac'))
|
||||
calculated_mac = self.hmac(self.km_o, macable + self.encode_mpi_with_padding(self.c_o))
|
||||
|
||||
if not calculated_mac == received_mac:
|
||||
raise exceptions.DecryptionError, 'bad signature'
|
||||
|
||||
m_final = base64.b64decode(c.getTagData('data'))
|
||||
m_compressed = self.decrypt(m_final)
|
||||
plaintext = self.decompress(m_compressed)
|
||||
|
||||
try:
|
||||
parsed = xmpp.Node(node='<node>' + plaintext + '</node>')
|
||||
except:
|
||||
raise exceptions.DecryptionError, 'decrypted <data/> not parseable as XML'
|
||||
|
||||
for child in parsed.getChildren():
|
||||
stanza.addChild(node=child)
|
||||
|
||||
return stanza
|
||||
|
||||
def decrypt(self, ciphertext):
|
||||
return self.decrypter.decrypt(ciphertext)
|
||||
|
||||
def logging_preference(self):
|
||||
if gajim.config.get('log_encrypted_sessions'):
|
||||
return ["may", "mustnot"]
|
||||
else:
|
||||
return ["mustnot", "may"]
|
||||
|
||||
def get_shared_secret(self, e, y, p):
|
||||
if (not 1 < e < (p - 1)):
|
||||
raise exceptions.NegotiationError, "invalid DH value"
|
||||
|
||||
return self.sha256(self.encode_mpi(self.powmod(e, y, p)))
|
||||
|
||||
def c7lize_mac_id(self, form):
|
||||
kids = form.getChildren()
|
||||
macable = filter(lambda x: x.getVar() not in ('mac', 'identity'), kids)
|
||||
return ''.join(map(lambda el: xmpp.c14n.c14n(el), macable))
|
||||
|
||||
def verify_identity(self, form, dh_i, sigmai, i_o):
|
||||
m_o = base64.b64decode(form['mac'])
|
||||
id_o = base64.b64decode(form['identity'])
|
||||
|
||||
m_o_calculated = self.hmac(self.km_o, self.encode_mpi(self.c_o) + id_o)
|
||||
|
||||
if m_o_calculated != m_o:
|
||||
raise exceptions.NegotiationError, 'calculated m_%s differs from received m_%s' % (i_o, i_o)
|
||||
|
||||
if i_o == 'a' and self.sas_algs == 'sas28x5':
|
||||
# XXX not necessary if there's a verified retained secret
|
||||
self.sas = self.sas_28x5(m_o, self.form_s)
|
||||
|
||||
if self.negotiated['recv_pubkey']:
|
||||
plaintext = self.decrypt(id_o)
|
||||
parsed = xmpp.Node(node='<node>' + plaintext + '</node>')
|
||||
|
||||
if self.negotiated['recv_pubkey'] == 'hash':
|
||||
fingerprint = parsed.getTagData('fingerprint')
|
||||
|
||||
# XXX find stored pubkey or terminate session
|
||||
raise 'unimplemented'
|
||||
else:
|
||||
if self.negotiated['sign_algs'] == (XmlDsig + 'rsa-sha256'):
|
||||
keyvalue = parsed.getTag(name='RSAKeyValue', namespace=XmlDsig)
|
||||
|
||||
n, e = map(lambda x: self.decode_mpi(base64.b64decode(keyvalue.getTagData(x))), ('Modulus', 'Exponent'))
|
||||
eir_pubkey = RSA.construct((n,long(e)))
|
||||
|
||||
pubkey_o = xmpp.c14n.c14n(keyvalue)
|
||||
else:
|
||||
# XXX DSA, etc.
|
||||
raise 'unimplemented'
|
||||
|
||||
enc_sig = parsed.getTag(name='SignatureValue', namespace=XmlDsig).getData()
|
||||
signature = (self.decode_mpi(base64.b64decode(enc_sig)),)
|
||||
else:
|
||||
mac_o = self.decrypt(id_o)
|
||||
pubkey_o = ''
|
||||
|
||||
c7l_form = self.c7lize_mac_id(form)
|
||||
|
||||
content = self.n_s + self.n_o + self.encode_mpi(dh_i) + pubkey_o
|
||||
|
||||
if sigmai:
|
||||
self.form_o = c7l_form
|
||||
content += self.form_o
|
||||
else:
|
||||
form_o2 = c7l_form
|
||||
content += self.form_o + form_o2
|
||||
|
||||
mac_o_calculated = self.hmac(self.ks_o, content)
|
||||
|
||||
if self.negotiated['recv_pubkey']:
|
||||
hash = self.sha256(mac_o_calculated)
|
||||
|
||||
if not eir_pubkey.verify(hash, signature):
|
||||
raise exceptions.NegotiationError, 'public key signature verification failed!'
|
||||
|
||||
elif mac_o_calculated != mac_o:
|
||||
raise exceptions.NegotiationError, 'calculated mac_%s differs from received mac_%s' % (i_o, i_o)
|
||||
|
||||
def make_identity(self, form, dh_i):
|
||||
if self.negotiated['send_pubkey']:
|
||||
if self.negotiated['sign_algs'] == (XmlDsig + 'rsa-sha256'):
|
||||
fields = (gajim.interface.pubkey.n, gajim.interface.pubkey.e)
|
||||
|
||||
cb_fields = map(lambda f: base64.b64encode(self.encode_mpi(f)), fields)
|
||||
|
||||
pubkey_s = '<RSAKeyValue xmlns="http://www.w3.org/2000/09/xmldsig#"><Modulus>%s</Modulus><Exponent>%s</Exponent></RSAKeyValue>' % tuple(cb_fields)
|
||||
else:
|
||||
pubkey_s = ''
|
||||
|
||||
form_s2 = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren()))
|
||||
|
||||
old_c_s = self.c_s
|
||||
content = self.n_o + self.n_s + self.encode_mpi(dh_i) + pubkey_s + self.form_s + form_s2
|
||||
|
||||
mac_s = self.hmac(self.ks_s, content)
|
||||
|
||||
if self.negotiated['send_pubkey']:
|
||||
signature = self.sign(mac_s)
|
||||
|
||||
sign_s = '<SignatureValue xmlns="http://www.w3.org/2000/09/xmldsig#">%s</SignatureValue>' % base64.b64encode(signature)
|
||||
|
||||
if self.negotiated['send_pubkey'] == 'hash':
|
||||
b64ed = base64.b64encode(self.hash(pubkey_s))
|
||||
pubkey_s = '<fingerprint>%s</fingerprint>' % b64ed
|
||||
|
||||
id_s = self.encrypt(pubkey_s + sign_s)
|
||||
else:
|
||||
id_s = self.encrypt(mac_s)
|
||||
|
||||
m_s = self.hmac(self.km_s, self.encode_mpi(old_c_s) + id_s)
|
||||
|
||||
if self.status == 'requested-e2e' and self.sas_algs == 'sas28x5':
|
||||
# we're alice; check for a retained secret
|
||||
# if none exists, prompt the user with the SAS
|
||||
self.sas = self.sas_28x5(m_s, self.form_o)
|
||||
|
||||
if self.sigmai:
|
||||
self.check_identity()
|
||||
|
||||
return (xmpp.DataField(name='identity', value=base64.b64encode(id_s)), \
|
||||
xmpp.DataField(name='mac', value=base64.b64encode(m_s)))
|
||||
|
||||
def negotiate_e2e(self, sigmai):
|
||||
self.negotiated = {}
|
||||
|
||||
request = xmpp.Message()
|
||||
feature = request.NT.feature
|
||||
feature.setNamespace(xmpp.NS_FEATURE)
|
||||
|
||||
x = xmpp.DataForm(typ='form')
|
||||
|
||||
x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn', typ='hidden'))
|
||||
x.addChild(node=xmpp.DataField(name='accept', value='1', typ='boolean', required=True))
|
||||
|
||||
# this field is incorrectly called 'otr' in XEPs 0116 and 0217
|
||||
x.addChild(node=xmpp.DataField(name='logging', typ='list-single', options=self.logging_preference(), required=True))
|
||||
|
||||
# unsupported options: 'disabled', 'enabled'
|
||||
x.addChild(node=xmpp.DataField(name='disclosure', typ='list-single', options=['never'], required=True))
|
||||
x.addChild(node=xmpp.DataField(name='security', typ='list-single', options=['e2e'], required=True))
|
||||
x.addChild(node=xmpp.DataField(name='crypt_algs', value='aes128-ctr', typ='hidden'))
|
||||
x.addChild(node=xmpp.DataField(name='hash_algs', value='sha256', typ='hidden'))
|
||||
x.addChild(node=xmpp.DataField(name='compress', value='none', typ='hidden'))
|
||||
|
||||
# unsupported options: 'iq', 'presence'
|
||||
x.addChild(node=xmpp.DataField(name='stanzas', typ='list-multi', options=['message']))
|
||||
|
||||
x.addChild(node=xmpp.DataField(name='init_pubkey', options=['none', 'key', 'hash'], typ='list-single'))
|
||||
|
||||
# XXX store key, use hash
|
||||
x.addChild(node=xmpp.DataField(name='resp_pubkey', options=['none', 'key'], typ='list-single'))
|
||||
|
||||
x.addChild(node=xmpp.DataField(name='ver', value='1.0', typ='hidden'))
|
||||
|
||||
x.addChild(node=xmpp.DataField(name='rekey_freq', value='4294967295', typ='hidden'))
|
||||
|
||||
x.addChild(node=xmpp.DataField(name='sas_algs', value='sas28x5', typ='hidden'))
|
||||
x.addChild(node=xmpp.DataField(name='sign_algs', value='http://www.w3.org/2000/09/xmldsig#rsa-sha256', typ='hidden'))
|
||||
|
||||
self.n_s = self.generate_nonce()
|
||||
|
||||
x.addChild(node=xmpp.DataField(name='my_nonce', value=base64.b64encode(self.n_s), typ='hidden'))
|
||||
|
||||
modp_options = [ 5, 14, 2, 1 ]
|
||||
|
||||
x.addChild(node=xmpp.DataField(name='modp', typ='list-single', options=map(lambda x: [ None, x ], modp_options)))
|
||||
|
||||
x.addChild(node=self.make_dhfield(modp_options, sigmai))
|
||||
self.sigmai = sigmai
|
||||
|
||||
self.form_s = ''.join(map(lambda el: xmpp.c14n.c14n(el), x.getChildren()))
|
||||
|
||||
feature.addChild(node=x)
|
||||
|
||||
self.status = 'requested-e2e'
|
||||
|
||||
self.send(request)
|
||||
|
||||
# 4.3 esession response (bob)
|
||||
def verify_options_bob(self, form):
|
||||
negotiated = {'recv_pubkey': None, 'send_pubkey': None}
|
||||
not_acceptable = []
|
||||
ask_user = {}
|
||||
|
||||
fixed = { 'disclosure': 'never',
|
||||
'security': 'e2e',
|
||||
'crypt_algs': 'aes128-ctr',
|
||||
'hash_algs': 'sha256',
|
||||
'compress': 'none',
|
||||
'stanzas': 'message',
|
||||
'init_pubkey': 'none',
|
||||
'resp_pubkey': 'none',
|
||||
'ver': '1.0',
|
||||
'sas_algs': 'sas28x5' }
|
||||
|
||||
self.encryptable_stanzas = ['message']
|
||||
|
||||
self.sas_algs = 'sas28x5'
|
||||
self.cipher = AES
|
||||
self.hash_alg = SHA256
|
||||
self.compression = None
|
||||
|
||||
for name, field in map(lambda name: (name, form.getField(name)), form.asDict().keys()):
|
||||
options = map(lambda x: x[1], field.getOptions())
|
||||
values = field.getValues()
|
||||
|
||||
if not field.getType() in ('list-single', 'list-multi'):
|
||||
options = values
|
||||
|
||||
if name in fixed:
|
||||
if fixed[name] in options:
|
||||
negotiated[name] = fixed[name]
|
||||
else:
|
||||
not_acceptable.append(name)
|
||||
elif name == 'rekey_freq':
|
||||
preferred = int(options[0])
|
||||
negotiated['rekey_freq'] = preferred
|
||||
self.rekey_freq = preferred
|
||||
elif name == 'logging':
|
||||
my_prefs = self.logging_preference()
|
||||
|
||||
if my_prefs[0] in options: # our first choice is offered, select it
|
||||
pref = my_prefs[0]
|
||||
negotiated['logging'] = pref
|
||||
else: # see if other acceptable choices are offered
|
||||
for pref in my_prefs:
|
||||
if pref in options:
|
||||
ask_user['logging'] = pref
|
||||
break
|
||||
|
||||
if not 'logging' in ask_user:
|
||||
not_acceptable.append(name)
|
||||
elif name == 'init_pubkey':
|
||||
for x in ('key'):
|
||||
if x in options:
|
||||
negotiated['recv_pubkey'] = x
|
||||
break
|
||||
elif name == 'resp_pubkey':
|
||||
for x in ('hash', 'key'):
|
||||
if x in options:
|
||||
negotiated['send_pubkey'] = x
|
||||
break
|
||||
elif name == 'sign_algs':
|
||||
if (XmlDsig + 'rsa-sha256') in options:
|
||||
negotiated['sign_algs'] = XmlDsig + 'rsa-sha256'
|
||||
else:
|
||||
# XXX some things are handled elsewhere, some things are not-implemented
|
||||
pass
|
||||
|
||||
return (negotiated, not_acceptable, ask_user)
|
||||
|
||||
# 4.3 esession response (bob)
|
||||
def respond_e2e_bob(self, form, negotiated, not_acceptable):
|
||||
response = xmpp.Message()
|
||||
feature = response.NT.feature
|
||||
feature.setNamespace(xmpp.NS_FEATURE)
|
||||
|
||||
x = xmpp.DataForm(typ='submit')
|
||||
|
||||
x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn'))
|
||||
x.addChild(node=xmpp.DataField(name='accept', value='true'))
|
||||
|
||||
for name in negotiated:
|
||||
# some fields are internal and should not be sent
|
||||
if not name in ('send_pubkey', 'recv_pubkey'):
|
||||
x.addChild(node=xmpp.DataField(name=name, value=negotiated[name]))
|
||||
|
||||
self.negotiated = negotiated
|
||||
|
||||
# the offset of the group we chose (need it to match up with the dhhash)
|
||||
group_order = 0
|
||||
self.modp = int(form.getField('modp').getOptions()[group_order][1])
|
||||
x.addChild(node=xmpp.DataField(name='modp', value=self.modp))
|
||||
|
||||
g = dh.generators[self.modp]
|
||||
p = dh.primes[self.modp]
|
||||
|
||||
self.n_o = base64.b64decode(form['my_nonce'])
|
||||
|
||||
dhhashes = form.getField('dhhashes').getValues()
|
||||
self.negotiated['He'] = base64.b64decode(dhhashes[group_order].encode("utf8"))
|
||||
|
||||
bytes = int(self.n / 8)
|
||||
|
||||
self.n_s = self.generate_nonce()
|
||||
|
||||
self.c_o = self.decode_mpi(self.random_bytes(bytes)) # n-bit random number
|
||||
self.c_s = self.c_o ^ (2 ** (self.n - 1))
|
||||
|
||||
self.y = self.srand(2 ** (2 * self.n - 1), p - 1)
|
||||
self.d = self.powmod(g, self.y, p)
|
||||
|
||||
to_add = { 'my_nonce': self.n_s,
|
||||
'dhkeys': self.encode_mpi(self.d),
|
||||
'counter': self.encode_mpi(self.c_o),
|
||||
'nonce': self.n_o }
|
||||
|
||||
for name in to_add:
|
||||
b64ed = base64.b64encode(to_add[name])
|
||||
x.addChild(node=xmpp.DataField(name=name, value=b64ed))
|
||||
|
||||
self.form_o = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren()))
|
||||
self.form_s = ''.join(map(lambda el: xmpp.c14n.c14n(el), x.getChildren()))
|
||||
|
||||
self.status = 'responded-e2e'
|
||||
|
||||
feature.addChild(node=x)
|
||||
|
||||
if not_acceptable:
|
||||
response = xmpp.Error(response, xmpp.ERR_NOT_ACCEPTABLE)
|
||||
|
||||
feature = xmpp.Node(xmpp.NS_FEATURE + ' feature')
|
||||
|
||||
for f in not_acceptable:
|
||||
n = xmpp.Node('field')
|
||||
n['var'] = f
|
||||
feature.addChild(node=n)
|
||||
|
||||
response.T.error.addChild(node=feature)
|
||||
|
||||
self.send(response)
|
||||
|
||||
# 'Alice Accepts'
|
||||
def verify_options_alice(self, form):
|
||||
negotiated = {}
|
||||
ask_user = {}
|
||||
not_acceptable = []
|
||||
|
||||
if not form['logging'] in self.logging_preference():
|
||||
not_acceptable.append(form['logging'])
|
||||
elif form['logging'] != self.logging_preference()[0]:
|
||||
ask_user['logging'] = form['logging']
|
||||
else:
|
||||
negotiated['logging'] = self.logging_preference()[0]
|
||||
|
||||
for r,a in (('recv_pubkey', 'resp_pubkey'), ('send_pubkey', 'init_pubkey')):
|
||||
negotiated[r] = None
|
||||
|
||||
if a in form.asDict() and form[a] in ('key', 'hash'):
|
||||
negotiated[r] = form[a]
|
||||
|
||||
if 'sign_algs' in form.asDict():
|
||||
if form['sign_algs'] in (XmlDsig + 'rsa-sha256',):
|
||||
negotiated['sign_algs'] = form['sign_algs']
|
||||
else:
|
||||
not_acceptable.append(form['sign_algs'])
|
||||
|
||||
return (negotiated, not_acceptable, ask_user)
|
||||
|
||||
# 'Alice Accepts', continued
|
||||
def accept_e2e_alice(self, form, negotiated):
|
||||
self.encryptable_stanzas = ['message']
|
||||
self.sas_algs = 'sas28x5'
|
||||
self.cipher = AES
|
||||
self.hash_alg = SHA256
|
||||
self.compression = None
|
||||
|
||||
self.negotiated = negotiated
|
||||
|
||||
accept = xmpp.Message()
|
||||
feature = accept.NT.feature
|
||||
feature.setNamespace(xmpp.NS_FEATURE)
|
||||
|
||||
result = xmpp.DataForm(typ='result')
|
||||
|
||||
self.c_s = self.decode_mpi(base64.b64decode(form['counter']))
|
||||
self.c_o = self.c_s ^ (2 ** (self.n - 1))
|
||||
|
||||
self.n_o = base64.b64decode(form['my_nonce'])
|
||||
|
||||
mod_p = int(form['modp'])
|
||||
p = dh.primes[mod_p]
|
||||
x = self.xes[mod_p]
|
||||
e = self.es[mod_p]
|
||||
|
||||
self.d = self.decode_mpi(base64.b64decode(form['dhkeys']))
|
||||
|
||||
self.k = self.get_shared_secret(self.d, x, p)
|
||||
|
||||
result.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn'))
|
||||
result.addChild(node=xmpp.DataField(name='accept', value='1'))
|
||||
result.addChild(node=xmpp.DataField(name='nonce', value=base64.b64encode(self.n_o)))
|
||||
|
||||
self.kc_s, self.km_s, self.ks_s = self.generate_initiator_keys(self.k)
|
||||
|
||||
if self.sigmai:
|
||||
self.kc_o, self.km_o, self.ks_o = self.generate_responder_keys(self.k)
|
||||
self.verify_identity(form, self.d, True, 'b')
|
||||
else:
|
||||
secrets = gajim.interface.list_secrets(self.conn.name, self.jid.getStripped())
|
||||
rshashes = [self.hmac(self.n_s, rs) for rs in secrets]
|
||||
|
||||
# XXX add some random fake rshashes here
|
||||
rshashes.sort()
|
||||
|
||||
rshashes = [base64.b64encode(rshash) for rshash in rshashes]
|
||||
result.addChild(node=xmpp.DataField(name='rshashes', value=rshashes))
|
||||
result.addChild(node=xmpp.DataField(name='dhkeys', value=base64.b64encode(self.encode_mpi(e))))
|
||||
|
||||
self.form_o = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren()))
|
||||
|
||||
|
||||
# MUST securely destroy K unless it will be used later to generate the final shared secret
|
||||
|
||||
for datafield in self.make_identity(result, e):
|
||||
result.addChild(node=datafield)
|
||||
|
||||
feature.addChild(node=result)
|
||||
self.send(accept)
|
||||
|
||||
if self.sigmai:
|
||||
self.status = 'active'
|
||||
self.enable_encryption = True
|
||||
else:
|
||||
self.status = 'identified-alice'
|
||||
|
||||
# 4.5 esession accept (bob)
|
||||
def accept_e2e_bob(self, form):
|
||||
response = xmpp.Message()
|
||||
|
||||
init = response.NT.init
|
||||
init.setNamespace(xmpp.NS_ESESSION_INIT)
|
||||
|
||||
x = xmpp.DataForm(typ='result')
|
||||
|
||||
for field in ('nonce', 'dhkeys', 'rshashes', 'identity', 'mac'):
|
||||
assert field in form.asDict(), "alice's form didn't have a %s field" % field
|
||||
|
||||
# 4.5.1 generating provisory session keys
|
||||
e = self.decode_mpi(base64.b64decode(form['dhkeys']))
|
||||
p = dh.primes[self.modp]
|
||||
|
||||
if self.sha256(self.encode_mpi(e)) != self.negotiated['He']:
|
||||
raise exceptions.NegotiationError, 'SHA256(e) != He'
|
||||
|
||||
k = self.get_shared_secret(e, self.y, p)
|
||||
|
||||
self.kc_o, self.km_o, self.ks_o = self.generate_initiator_keys(k)
|
||||
|
||||
# 4.5.2 verifying alice's identity
|
||||
|
||||
self.verify_identity(form, e, False, 'a')
|
||||
|
||||
# 4.5.4 generating bob's final session keys
|
||||
|
||||
srs = ''
|
||||
|
||||
secrets = gajim.interface.list_secrets(self.conn.name, self.jid.getStripped())
|
||||
rshashes = [base64.b64decode(rshash) for rshash in form.getField('rshashes').getValues()]
|
||||
|
||||
for secret in secrets:
|
||||
if self.hmac(self.n_o, secret) in rshashes:
|
||||
srs = secret
|
||||
break
|
||||
|
||||
# other shared secret, we haven't got one.
|
||||
oss = ''
|
||||
|
||||
k = self.sha256(k + srs + oss)
|
||||
|
||||
self.kc_s, self.km_s, self.ks_s = self.generate_responder_keys(k)
|
||||
self.kc_o, self.km_o, self.ks_o = self.generate_initiator_keys(k)
|
||||
|
||||
# 4.5.5
|
||||
if srs:
|
||||
srshash = self.hmac(srs, 'Shared Retained Secret')
|
||||
else:
|
||||
srshash = self.random_bytes(32)
|
||||
|
||||
x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn'))
|
||||
x.addChild(node=xmpp.DataField(name='nonce', value=base64.b64encode(self.n_o)))
|
||||
x.addChild(node=xmpp.DataField(name='srshash', value=base64.b64encode(srshash)))
|
||||
|
||||
for datafield in self.make_identity(x, self.d):
|
||||
x.addChild(node=datafield)
|
||||
|
||||
init.addChild(node=x)
|
||||
|
||||
self.send(response)
|
||||
|
||||
self.do_retained_secret(k, srs)
|
||||
|
||||
if self.negotiated['logging'] == 'mustnot':
|
||||
self.loggable = False
|
||||
|
||||
self.status = 'active'
|
||||
self.enable_encryption = True
|
||||
|
||||
def final_steps_alice(self, form):
|
||||
srs = ''
|
||||
secrets = gajim.interface.list_secrets(self.conn.name, self.jid.getStripped())
|
||||
|
||||
srshash = base64.b64decode(form['srshash'])
|
||||
|
||||
for secret in secrets:
|
||||
if self.hmac(secret, 'Shared Retained Secret') == srshash:
|
||||
srs = secret
|
||||
break
|
||||
|
||||
oss = ''
|
||||
k = self.sha256(self.k + srs + oss)
|
||||
del self.k
|
||||
|
||||
self.do_retained_secret(k, srs)
|
||||
|
||||
# don't need to calculate ks_s here
|
||||
|
||||
self.kc_s, self.km_s, self.ks_s = self.generate_initiator_keys(k)
|
||||
self.kc_o, self.km_o, self.ks_o = self.generate_responder_keys(k)
|
||||
|
||||
# 4.6.2 Verifying Bob's Identity
|
||||
|
||||
self.verify_identity(form, self.d, False, 'b')
|
||||
# Note: If Alice discovers an error then she SHOULD ignore any encrypted content she received in the stanza.
|
||||
|
||||
if self.negotiated['logging'] == 'mustnot':
|
||||
self.loggable = False
|
||||
|
||||
self.status = 'active'
|
||||
self.enable_encryption = True
|
||||
|
||||
# calculate and store the new retained secret
|
||||
# prompt the user to check the remote party's identity (if necessary)
|
||||
def do_retained_secret(self, k, srs):
|
||||
new_srs = self.hmac(k, 'New Retained Secret')
|
||||
account = self.conn.name
|
||||
bjid = self.jid.getStripped()
|
||||
|
||||
if srs:
|
||||
gajim.interface.replace_secret(account, bjid, srs, new_srs)
|
||||
else:
|
||||
self.check_identity()
|
||||
gajim.interface.save_new_secret(account, bjid, new_srs)
|
||||
|
||||
# generate a random number between 'bottom' and 'top'
|
||||
def srand(self, bottom, top):
|
||||
# minimum number of bytes needed to represent that range
|
||||
bytes = int(math.ceil(math.log(top - bottom, 256)))
|
||||
|
||||
# in retrospect, this is horribly inadequate.
|
||||
return (self.decode_mpi(self.random_bytes(bytes)) % (top - bottom)) + bottom
|
||||
|
||||
def make_dhfield(self, modp_options, sigmai):
|
||||
dhs = []
|
||||
|
||||
for modp in modp_options:
|
||||
p = dh.primes[modp]
|
||||
g = dh.generators[modp]
|
||||
|
||||
x = self.srand(2 ** (2 * self.n - 1), p - 1)
|
||||
|
||||
# XXX this may be a source of performance issues
|
||||
e = self.powmod(g, x, p)
|
||||
|
||||
self.xes[modp] = x
|
||||
self.es[modp] = e
|
||||
|
||||
if sigmai:
|
||||
dhs.append(base64.b64encode(self.encode_mpi(e)))
|
||||
name = 'dhkeys'
|
||||
else:
|
||||
He = self.sha256(self.encode_mpi(e))
|
||||
dhs.append(base64.b64encode(He))
|
||||
name = 'dhhashes'
|
||||
|
||||
return xmpp.DataField(name=name, typ='hidden', value=dhs)
|
||||
|
||||
# a faster version of (base ** exp) % mod
|
||||
# taken from <http://lists.danga.com/pipermail/yadis/2005-September/001445.html>
|
||||
def powmod(self, base, exp, mod):
|
||||
square = base % mod
|
||||
result = 1
|
||||
|
||||
while exp > 0:
|
||||
if exp & 1: # exponent is odd
|
||||
result = (result * square) % mod
|
||||
|
||||
square = (square * square) % mod
|
||||
exp /= 2
|
||||
|
||||
return result
|
||||
|
||||
def terminate_e2e(self):
|
||||
self.terminate()
|
||||
|
||||
self.enable_encryption = False
|
||||
|
||||
def acknowledge_termination(self):
|
||||
StanzaSession.acknowledge_termination(self)
|
||||
|
||||
self.enable_encryption = False
|
||||
|
||||
def fail_bad_negotiation(self, reason):
|
||||
'''they've tried to feed us a bogus value, send an error and cancel everything.'''
|
||||
|
||||
err = xmpp.Error(xmpp.Message(), xmpp.ERR_FEATURE_NOT_IMPLEMENTED)
|
||||
err.T.error.T.text.setData(reason)
|
||||
self.send(err)
|
||||
|
||||
self.status = None
|
||||
self.enable_encryption = False
|
||||
|
||||
# this prevents the MAC check on decryption from succeeding,
|
||||
# preventing falsified messages from going through.
|
||||
self.km_o = ''
|
||||
|
||||
def is_loggable(self):
|
||||
account = self.conn.name
|
||||
no_log_for = gajim.config.get_per('accounts', account, 'no_log_for')
|
||||
|
||||
if not no_log_for:
|
||||
no_log_for = ''
|
||||
|
||||
no_log_for = no_log_for.split()
|
||||
|
||||
return self.loggable and account not in no_log_for and self.jid not in no_log_for
|
|
@ -0,0 +1,36 @@
|
|||
from simplexml import ustr
|
||||
|
||||
# XML canonicalisation methods (for XEP-0116)
|
||||
def c14n(node):
|
||||
s = "<" + node.name
|
||||
if node.namespace:
|
||||
if not node.parent or node.parent.namespace != node.namespace:
|
||||
s = s + ' xmlns="%s"' % node.namespace
|
||||
|
||||
sorted_attrs = node.attrs.keys()
|
||||
sorted_attrs.sort()
|
||||
for key in sorted_attrs:
|
||||
val = ustr(node.attrs[key])
|
||||
# like XMLescape() but with whitespace and without >
|
||||
s = s + ' %s="%s"' % ( key, normalise_attr(val) )
|
||||
s = s + ">"
|
||||
cnt = 0
|
||||
if node.kids:
|
||||
for a in node.kids:
|
||||
if (len(node.data)-1) >= cnt:
|
||||
s = s + normalise_text(node.data[cnt])
|
||||
s = s + c14n(a)
|
||||
cnt=cnt+1
|
||||
if (len(node.data)-1) >= cnt: s = s + normalise_text(node.data[cnt])
|
||||
if not node.kids and s[-1:]=='>':
|
||||
s=s[:-1]+' />'
|
||||
else:
|
||||
s = s + "</" + node.name + ">"
|
||||
return s.encode('utf-8')
|
||||
|
||||
def normalise_attr(val):
|
||||
return val.replace('&', '&').replace('<', '<').replace('"', '"').replace('\t', '	').replace('\n', '
').replace('\r', '
')
|
||||
|
||||
def normalise_text(val):
|
||||
return val.replace('&', '&').replace('<', '<').replace('>', '>').replace('\r', '
')
|
||||
|
|
@ -45,6 +45,7 @@ NS_DISCO ='http://jabber.org/protocol/disco'
|
|||
NS_DISCO_INFO =NS_DISCO+'#info'
|
||||
NS_DISCO_ITEMS =NS_DISCO+'#items'
|
||||
NS_ENCRYPTED ='jabber:x:encrypted' # XEP-0027
|
||||
NS_ESESSION_INIT='http://www.xmpp.org/extensions/xep-0116.html#ns-init' # XEP-0116
|
||||
NS_EVENT ='jabber:x:event' # XEP-0022
|
||||
NS_FEATURE ='http://jabber.org/protocol/feature-neg'
|
||||
NS_FILE ='http://jabber.org/protocol/si/profile/file-transfer' # XEP-0096
|
||||
|
@ -82,6 +83,7 @@ NS_SESSION ='urn:ietf:params:xml:ns:xmpp-session'
|
|||
NS_SI ='http://jabber.org/protocol/si' # XEP-0096
|
||||
NS_SI_PUB ='http://jabber.org/protocol/sipub' # XEP-0137
|
||||
NS_SIGNED ='jabber:x:signed' # XEP-0027
|
||||
NS_STANZA_CRYPTO='http://www.xmpp.org/extensions/xep-0200.html#ns' # JEP-0200
|
||||
NS_STANZAS ='urn:ietf:params:xml:ns:xmpp-stanzas'
|
||||
NS_STREAM ='http://affinix.com/jabber/stream'
|
||||
NS_STREAMS ='http://etherx.jabber.org/streams'
|
||||
|
|
|
@ -639,6 +639,7 @@ class ConnectionHandlersZeroconf(ConnectionVcard, ConnectionBytestream):
|
|||
msghtml = msg.getXHTML()
|
||||
mtype = msg.getType()
|
||||
subject = msg.getSubject() # if not there, it's None
|
||||
thread = msg.getThread()
|
||||
tim = msg.getTimestamp()
|
||||
tim = helpers.datetime_tuple(tim)
|
||||
tim = time.localtime(timegm(tim))
|
||||
|
@ -715,7 +716,7 @@ class ConnectionHandlersZeroconf(ConnectionVcard, ConnectionBytestream):
|
|||
msg_id = gajim.logger.write('chat_msg_recv', frm, msgtxt, tim = tim,
|
||||
subject = subject)
|
||||
self.dispatch('MSG', (frm, msgtxt, tim, encrypted, mtype, subject,
|
||||
chatstate, msg_id, composing_xep, user_nick, msghtml))
|
||||
chatstate, msg_id, composing_jep, user_nick, msghtml, thread))
|
||||
elif mtype == 'normal': # it's single message
|
||||
if self.name not in no_log_for and jid not in no_log_for and msgtxt:
|
||||
gajim.logger.write('single_msg_recv', frm, msgtxt, tim = tim,
|
||||
|
|
|
@ -199,7 +199,7 @@ class EditGroupsDialog:
|
|||
for group in groups:
|
||||
if group not in helpers.special_groups or groups[group] > 0:
|
||||
group_list.append(group)
|
||||
group_list.sort()
|
||||
group_list.sort()
|
||||
for group in group_list:
|
||||
iter = store.append()
|
||||
store.set(iter, 0, group) # Group name
|
||||
|
@ -1427,7 +1427,7 @@ class SynchroniseSelectAccountDialog:
|
|||
if not iter:
|
||||
return
|
||||
remote_account = model.get_value(iter, 0).decode('utf-8')
|
||||
|
||||
|
||||
if gajim.connections[remote_account].connected < 2:
|
||||
ErrorDialog(_('This account is not connected to the server'),
|
||||
_('You cannot synchronize with an account unless it is connected.'))
|
||||
|
@ -1709,7 +1709,7 @@ class SingleMessageWindow:
|
|||
or 'receive'.
|
||||
'''
|
||||
def __init__(self, account, to = '', action = '', from_whom = '',
|
||||
subject = '', message = '', resource = ''):
|
||||
subject = '', message = '', resource = '', session = None):
|
||||
self.account = account
|
||||
self.action = action
|
||||
|
||||
|
@ -1718,6 +1718,7 @@ class SingleMessageWindow:
|
|||
self.to = to
|
||||
self.from_whom = from_whom
|
||||
self.resource = resource
|
||||
self.session = session
|
||||
|
||||
self.xml = gtkgui_helpers.get_glade('single_message_window.glade')
|
||||
self.window = self.xml.get_widget('single_message_window')
|
||||
|
@ -1908,7 +1909,7 @@ class SingleMessageWindow:
|
|||
|
||||
# FIXME: allow GPG message some day
|
||||
gajim.connections[self.account].send_message(to_whom_jid, message,
|
||||
keyID = None, type = 'normal', subject=subject)
|
||||
keyID = None, type = 'normal', subject=subject, session = self.session)
|
||||
|
||||
self.subject_entry.set_text('') # we sent ok, clear the subject
|
||||
self.message_tv_buffer.set_text('') # we sent ok, clear the textview
|
||||
|
@ -1925,7 +1926,7 @@ class SingleMessageWindow:
|
|||
self.window.destroy()
|
||||
SingleMessageWindow(self.account, to = self.from_whom,
|
||||
action = 'send', from_whom = self.from_whom, subject = self.subject,
|
||||
message = self.message)
|
||||
message = self.message, session = self.session)
|
||||
|
||||
def on_send_and_close_button_clicked(self, widget):
|
||||
self.send_single_message()
|
||||
|
@ -2111,7 +2112,6 @@ class PrivacyListWindow:
|
|||
jid_entry_completion.set_model(jids_list_store)
|
||||
jid_entry_completion.set_popup_completion(True)
|
||||
self.edit_type_jabberid_entry.set_completion(jid_entry_completion)
|
||||
|
||||
if action == 'EDIT':
|
||||
self.refresh_rules()
|
||||
|
||||
|
|
229
src/gajim.py
229
src/gajim.py
|
@ -117,12 +117,17 @@ import message_control
|
|||
from chat_control import ChatControlBase
|
||||
from atom_window import AtomWindow
|
||||
|
||||
import negotiation
|
||||
import Crypto.PublicKey.RSA
|
||||
|
||||
from common import exceptions
|
||||
from common.zeroconf import connection_zeroconf
|
||||
from common import dbus_support
|
||||
if dbus_support.supported:
|
||||
import dbus
|
||||
|
||||
import pickle
|
||||
|
||||
if os.name == 'posix': # dl module is Unix Only
|
||||
try: # rename the process name to gajim
|
||||
import dl
|
||||
|
@ -217,6 +222,7 @@ gajimpaths = common.configpaths.gajimpaths
|
|||
|
||||
pid_filename = gajimpaths['PID_FILE']
|
||||
config_filename = gajimpaths['CONFIG_FILE']
|
||||
secrets_filename = gajimpaths['SECRETS_FILE']
|
||||
|
||||
import traceback
|
||||
import errno
|
||||
|
@ -686,11 +692,11 @@ class Interface:
|
|||
# It's maybe a GC_NOTIFY (specialy for MSN gc)
|
||||
self.handle_event_gc_notify(account, (jid, array[1], status_message,
|
||||
array[3], None, None, None, None, None, None, None, None))
|
||||
|
||||
|
||||
|
||||
def handle_event_msg(self, account, array):
|
||||
# 'MSG' (account, (jid, msg, time, encrypted, msg_type, subject,
|
||||
# chatstate, msg_id, composing_xep, user_nick, xhtml))
|
||||
# chatstate, msg_id, composing_xep, user_nick, xhtml, session))
|
||||
# user_nick is JEP-0172
|
||||
|
||||
full_jid_with_resource = array[0]
|
||||
|
@ -705,6 +711,7 @@ class Interface:
|
|||
msg_id = array[7]
|
||||
composing_xep = array[8]
|
||||
xhtml = array[10]
|
||||
session = array[11]
|
||||
if gajim.config.get('ignore_incoming_xhtml'):
|
||||
xhtml = None
|
||||
if gajim.jid_is_transport(jid):
|
||||
|
@ -791,20 +798,20 @@ class Interface:
|
|||
if pm:
|
||||
nickname = resource
|
||||
groupchat_control.on_private_message(nickname, message, array[2],
|
||||
xhtml, msg_id)
|
||||
xhtml, session, msg_id)
|
||||
else:
|
||||
# array: (jid, msg, time, encrypted, msg_type, subject)
|
||||
if encrypted:
|
||||
self.roster.on_message(jid, message, array[2], account, array[3],
|
||||
msg_type, subject, resource, msg_id, array[9],
|
||||
advanced_notif_num)
|
||||
advanced_notif_num, session = session)
|
||||
else:
|
||||
# xhtml in last element
|
||||
self.roster.on_message(jid, message, array[2], account, array[3],
|
||||
msg_type, subject, resource, msg_id, array[9],
|
||||
advanced_notif_num, xhtml = xhtml)
|
||||
advanced_notif_num, xhtml = xhtml, session = session)
|
||||
nickname = gajim.get_name_from_jid(account, jid)
|
||||
# Check and do wanted notifications
|
||||
# Check and do wanted notifications
|
||||
msg = message
|
||||
if subject:
|
||||
msg = _('Subject: %s') % subject + '\n' + msg
|
||||
|
@ -1501,6 +1508,48 @@ class Interface:
|
|||
if os.path.isfile(path_to_original_file):
|
||||
os.remove(path_to_original_file)
|
||||
|
||||
# list the retained secrets we have for a local account and a remote jid
|
||||
def list_secrets(self, account, jid):
|
||||
f = open(secrets_filename)
|
||||
|
||||
try:
|
||||
s = pickle.load(f)[account][jid]
|
||||
except KeyError:
|
||||
s = []
|
||||
|
||||
f.close()
|
||||
return s
|
||||
|
||||
# save a new retained secret
|
||||
def save_new_secret(self, account, jid, secret):
|
||||
f = open(secrets_filename, 'r')
|
||||
secrets = pickle.load(f)
|
||||
f.close()
|
||||
|
||||
if not account in secrets:
|
||||
secrets[account] = {}
|
||||
|
||||
if not jid in secrets[account]:
|
||||
secrets[account][jid] = []
|
||||
|
||||
secrets[account][jid].append(secret)
|
||||
|
||||
f = open(secrets_filename, 'w')
|
||||
pickle.dump(secrets, f)
|
||||
f.close()
|
||||
|
||||
def replace_secret(self, account, jid, old_secret, new_secret):
|
||||
f = open(secrets_filename, 'r')
|
||||
secrets = pickle.load(f)
|
||||
f.close()
|
||||
|
||||
this_secrets = secrets[account][jid]
|
||||
this_secrets[this_secrets.index(old_secret)] = new_secret
|
||||
|
||||
f = open(secrets_filename, 'w')
|
||||
pickle.dump(secrets, f)
|
||||
f.close()
|
||||
|
||||
def add_event(self, account, jid, type_, event_args):
|
||||
'''add an event to the gajim.events var'''
|
||||
# We add it to the gajim.events queue
|
||||
|
@ -1768,6 +1817,162 @@ class Interface:
|
|||
atom_entry, = data
|
||||
AtomWindow.newAtomEntry(atom_entry)
|
||||
|
||||
def handle_event_failed_decrypt(self, account, data):
|
||||
jid, tim = data
|
||||
|
||||
ctrl = self.msg_win_mgr.get_control(jid, account)
|
||||
if ctrl:
|
||||
ctrl.print_conversation_line('Unable to decrypt message from %s\nIt may have been tampered with.' % (jid), 'status', '', tim)
|
||||
else:
|
||||
print 'failed decrypt, unable to find a control to notify you in.'
|
||||
|
||||
def handle_session_negotiation(self, account, data):
|
||||
jid, session, form = data
|
||||
|
||||
if form.getField('accept') and not form['accept'] in ('1', 'true'):
|
||||
dialogs.InformationDialog(_('Session negotiation cancelled'),
|
||||
_('The client at %s cancelled the session negotiation.') % (jid))
|
||||
session.cancelled_negotiation()
|
||||
return
|
||||
|
||||
# encrypted session states. these are described in stanza_session.py
|
||||
|
||||
# bob responds
|
||||
if form.getType() == 'form' and 'security' in form.asDict():
|
||||
def continue_with_negotiation(*args):
|
||||
if len(args):
|
||||
self.dialog.destroy()
|
||||
|
||||
# we don't support 3-message negotiation as the responder
|
||||
if 'dhkeys' in form.asDict():
|
||||
err = xmpp.Error(xmpp.Message(), xmpp.ERR_FEATURE_NOT_IMPLEMENTED)
|
||||
|
||||
feature = xmpp.Node(xmpp.NS_FEATURE + ' feature')
|
||||
field = xmpp.Node('field')
|
||||
field['var'] = 'dhkeys'
|
||||
|
||||
feature.addChild(node=field)
|
||||
err.addChild(node=feature)
|
||||
|
||||
session.send(err)
|
||||
return
|
||||
|
||||
negotiated, not_acceptable, ask_user = session.verify_options_bob(form)
|
||||
|
||||
if ask_user:
|
||||
def accept_nondefault_options(widget):
|
||||
self.dialog.destroy()
|
||||
negotiated.update(ask_user)
|
||||
session.respond_e2e_bob(form, negotiated, not_acceptable)
|
||||
|
||||
def reject_nondefault_options(widget):
|
||||
self.dialog.destroy()
|
||||
for key in ask_user.keys():
|
||||
not_acceptable.append(key)
|
||||
session.respond_e2e_bob(form, negotiated, not_acceptable)
|
||||
|
||||
self.dialog = dialogs.YesNoDialog(_('Confirm these session options'),
|
||||
_('''The remote client wants to negotiate an session with these features:
|
||||
|
||||
%s
|
||||
|
||||
Are these options acceptable?''') % (negotiation.describe_features(ask_user)),
|
||||
on_response_yes = accept_nondefault_options,
|
||||
on_response_no = reject_nondefault_options)
|
||||
else:
|
||||
session.respond_e2e_bob(form, negotiated, not_acceptable)
|
||||
|
||||
def ignore_negotiation(widget):
|
||||
self.dialog.destroy()
|
||||
return
|
||||
|
||||
continue_with_negotiation()
|
||||
|
||||
return
|
||||
|
||||
# alice accepts
|
||||
elif session.status == 'requested-e2e' and form.getType() == 'submit':
|
||||
negotiated, not_acceptable, ask_user = session.verify_options_alice(form)
|
||||
|
||||
if session.sigmai:
|
||||
session.check_identity = lambda: negotiation.show_sas_dialog(jid, session.sas)
|
||||
|
||||
if ask_user:
|
||||
def accept_nondefault_options(widget):
|
||||
dialog.destroy()
|
||||
|
||||
negotiated.update(ask_user)
|
||||
|
||||
try:
|
||||
session.accept_e2e_alice(form, negotiated)
|
||||
except exceptions.NegotiationError, details:
|
||||
session.fail_bad_negotiation(details)
|
||||
|
||||
def reject_nondefault_options(widget):
|
||||
session.reject_negotiation()
|
||||
dialog.destroy()
|
||||
|
||||
dialog = dialogs.YesNoDialog(_('Confirm these session options'),
|
||||
_('The remote client selected these options:\n\n%s\n\nContinue with the session?') % (negotiation.describe_features(ask_user)),
|
||||
on_response_yes = accept_nondefault_options,
|
||||
on_response_no = reject_nondefault_options)
|
||||
else:
|
||||
try:
|
||||
session.accept_e2e_alice(form, negotiated)
|
||||
except exceptions.NegotiationError, details:
|
||||
session.fail_bad_negotiation(details)
|
||||
|
||||
return
|
||||
elif session.status == 'responded-e2e' and form.getType() == 'result':
|
||||
session.check_identity = lambda: negotiation.show_sas_dialog(jid, session.sas)
|
||||
|
||||
try:
|
||||
session.accept_e2e_bob(form)
|
||||
except exceptions.NegotiationError, details:
|
||||
session.fail_bad_negotiation(details)
|
||||
|
||||
return
|
||||
elif session.status == 'identified-alice' and form.getType() == 'result':
|
||||
session.check_identity = lambda: negotiation.show_sas_dialog(jid, session.sas)
|
||||
|
||||
try:
|
||||
session.final_steps_alice(form)
|
||||
except exceptions.NegotiationError, details:
|
||||
session.fail_bad_negotiation(details)
|
||||
|
||||
return
|
||||
|
||||
if form.getField('terminate'):
|
||||
if form.getField('terminate').getValue() in ('1', 'true'):
|
||||
session.acknowledge_termination()
|
||||
|
||||
gajim.connections[account].delete_session(str(jid), session.thread_id)
|
||||
|
||||
ctrl = gajim.interface.msg_win_mgr.get_control(str(jid), account)
|
||||
|
||||
if ctrl:
|
||||
ctrl.session = gajim.connections[account].make_new_session(str(jid))
|
||||
|
||||
return
|
||||
|
||||
# non-esession negotiation. this isn't very useful, but i'm keeping it around
|
||||
# to test my test suite.
|
||||
if form.getType() == 'form':
|
||||
ctrl = gajim.interface.msg_win_mgr.get_control(str(jid), account)
|
||||
if not ctrl:
|
||||
resource = jid.getResource()
|
||||
contact = gajim.contacts.get_contact(account, str(jid), resource)
|
||||
if not contact:
|
||||
connection = gajim.connections[account]
|
||||
contact = gajim.contacts.create_contact(jid = jid.getStripped(), resource = resource, show = connection.get_status())
|
||||
self.roster.new_chat(contact, account, resource = resource)
|
||||
|
||||
ctrl = gajim.interface.msg_win_mgr.get_control(str(jid), account)
|
||||
|
||||
ctrl.set_session(session)
|
||||
|
||||
negotiation.FeatureNegotiationWindow(account, jid, session, form)
|
||||
|
||||
def handle_event_privacy_lists_received(self, account, data):
|
||||
# ('PRIVACY_LISTS_RECEIVED', account, list)
|
||||
if not self.instances.has_key(account):
|
||||
|
@ -2208,6 +2413,7 @@ class Interface:
|
|||
'SIGNED_IN': self.handle_event_signed_in,
|
||||
'METACONTACTS': self.handle_event_metacontacts,
|
||||
'ATOM_ENTRY': self.handle_atom_entry,
|
||||
'FAILED_DECRYPT': self.handle_event_failed_decrypt,
|
||||
'PRIVACY_LISTS_RECEIVED': self.handle_event_privacy_lists_received,
|
||||
'PRIVACY_LIST_RECEIVED': self.handle_event_privacy_list_received,
|
||||
'PRIVACY_LISTS_ACTIVE_DEFAULT': \
|
||||
|
@ -2223,6 +2429,7 @@ class Interface:
|
|||
'UNIQUE_ROOM_ID_UNSUPPORTED': \
|
||||
self.handle_event_unique_room_id_unsupported,
|
||||
'UNIQUE_ROOM_ID_SUPPORTED': self.handle_event_unique_room_id_supported,
|
||||
'SESSION_NEG': self.handle_session_negotiation,
|
||||
}
|
||||
gajim.handlers = self.handlers
|
||||
|
||||
|
@ -2545,6 +2752,10 @@ class Interface:
|
|||
gobject.timeout_add(2000, self.process_connections)
|
||||
gobject.timeout_add(10000, self.read_sleepy)
|
||||
|
||||
# public key for XEP-0116
|
||||
# XXX os.urandom is not a cryptographic PRNG
|
||||
self.pubkey = Crypto.PublicKey.RSA.generate(384, os.urandom)
|
||||
|
||||
if __name__ == '__main__':
|
||||
def sigint_cb(num, stack):
|
||||
sys.exit(5)
|
||||
|
@ -2583,5 +2794,11 @@ if __name__ == '__main__':
|
|||
|
||||
check_paths.check_and_possibly_create_paths()
|
||||
|
||||
# create secrets file (unless it exists)
|
||||
if not os.path.exists(secrets_filename):
|
||||
f = open(secrets_filename, 'w')
|
||||
pickle.dump({}, f)
|
||||
f.close()
|
||||
|
||||
Interface()
|
||||
gtk.main()
|
||||
|
|
|
@ -105,14 +105,14 @@ def tree_cell_data_func(column, renderer, model, iter, tv=None):
|
|||
class PrivateChatControl(ChatControl):
|
||||
TYPE_ID = message_control.TYPE_PM
|
||||
|
||||
def __init__(self, parent_win, gc_contact, contact, account):
|
||||
def __init__(self, parent_win, gc_contact, contact, account, session):
|
||||
room_jid = contact.jid.split('/')[0]
|
||||
room_ctrl = gajim.interface.msg_win_mgr.get_control(room_jid, account)
|
||||
if gajim.interface.minimized_controls[account].has_key(room_jid):
|
||||
room_ctrl = gajim.interface.minimized_controls[account][room_jid]
|
||||
self.room_name = room_ctrl.name
|
||||
self.gc_contact = gc_contact
|
||||
ChatControl.__init__(self, parent_win, contact, account)
|
||||
ChatControl.__init__(self, parent_win, contact, account, session)
|
||||
self.TYPE_ID = 'pm'
|
||||
|
||||
def send_message(self, message):
|
||||
|
@ -544,7 +544,7 @@ class GroupchatControl(ChatControlBase):
|
|||
else:
|
||||
self.print_conversation(msg, nick, tim, xhtml)
|
||||
|
||||
def on_private_message(self, nick, msg, tim, xhtml, msg_id = None):
|
||||
def on_private_message(self, nick, msg, tim, xhtml, session, msg_id = None):
|
||||
# Do we have a queue?
|
||||
fjid = self.room_jid + '/' + nick
|
||||
no_queue = len(gajim.events.get_events(self.account, fjid)) == 0
|
||||
|
@ -556,7 +556,7 @@ class GroupchatControl(ChatControlBase):
|
|||
return
|
||||
|
||||
event = gajim.events.create_event('pm', (msg, '', 'incoming', tim,
|
||||
False, '', msg_id, xhtml))
|
||||
False, '', msg_id, xhtml, session))
|
||||
gajim.events.add_event(self.account, fjid, event)
|
||||
|
||||
autopopup = gajim.config.get('autopopup')
|
||||
|
@ -576,7 +576,7 @@ class GroupchatControl(ChatControlBase):
|
|||
self.parent_win.show_title()
|
||||
self.parent_win.redraw_tab(self)
|
||||
else:
|
||||
self._start_private_message(nick)
|
||||
self._start_private_message(nick, session)
|
||||
# Scroll to line
|
||||
self.list_treeview.expand_row(path[0:1], False)
|
||||
self.list_treeview.scroll_to_cell(path)
|
||||
|
@ -1942,7 +1942,7 @@ class GroupchatControl(ChatControlBase):
|
|||
menu.show_all()
|
||||
menu.popup(None, None, None, event.button, event.time)
|
||||
|
||||
def _start_private_message(self, nick):
|
||||
def _start_private_message(self, nick, session = None):
|
||||
gc_c = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick)
|
||||
nick_jid = gc_c.get_full_jid()
|
||||
|
||||
|
|
|
@ -110,14 +110,28 @@ class MessageControl:
|
|||
def get_specific_unread(self):
|
||||
return len(gajim.events.get_events(self.account, self.contact.jid))
|
||||
|
||||
def set_session(self, session):
|
||||
if session == self.session:
|
||||
return
|
||||
|
||||
if self.session:
|
||||
print "starting a new session, dropping the old one!"
|
||||
gajim.connections[self.account].delete_session(self.contact.get_full_jid(), self.session.thread_id)
|
||||
|
||||
self.session = session
|
||||
|
||||
def send_message(self, message, keyID = '', type = 'chat',
|
||||
chatstate = None, msg_id = None, composing_xep = None, resource = None,
|
||||
user_nick = None):
|
||||
'''Send the given message to the active tab. Doesn't return None if error
|
||||
'''
|
||||
jid = self.contact.jid
|
||||
|
||||
if not self.session:
|
||||
self.session = gajim.connections[self.account].make_new_session(self.contact.get_full_jid())
|
||||
|
||||
# Send and update history
|
||||
return gajim.connections[self.account].send_message(jid, message, keyID,
|
||||
type = type, chatstate = chatstate, msg_id = msg_id,
|
||||
composing_xep = composing_xep, resource = self.resource,
|
||||
user_nick = user_nick)
|
||||
user_nick = user_nick, session = self.session)
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
import gtkgui_helpers
|
||||
import dataforms_widget
|
||||
|
||||
import dialogs
|
||||
|
||||
from common import dataforms
|
||||
from common import gajim
|
||||
from common import xmpp
|
||||
|
||||
def describe_features(features):
|
||||
'''a human-readable description of the features that have been negotiated'''
|
||||
if features['logging'] == 'may':
|
||||
return _('- messages will be logged')
|
||||
elif features['logging'] == 'mustnot':
|
||||
return _('- messages will not be logged')
|
||||
|
||||
def show_sas_dialog(jid, sas):
|
||||
dialogs.InformationDialog(_('''Verify the remote client's identity'''), _('''You've begun an encrypted session with %s, but it can't be guaranteed that you're talking directly to the person you think you are.
|
||||
|
||||
You should speak with them directly (in person or on the phone) and confirm that their Short Authentication String is identical to this one: %s''') % (jid, sas))
|
||||
|
||||
class FeatureNegotiationWindow:
|
||||
'''FeatureNegotiotionWindow class'''
|
||||
def __init__(self, account, jid, session, form):
|
||||
self.account = account
|
||||
self.jid = jid
|
||||
self.form = form
|
||||
self.session = session
|
||||
|
||||
self.xml = gtkgui_helpers.get_glade('data_form_window.glade', 'data_form_window')
|
||||
self.window = self.xml.get_widget('data_form_window')
|
||||
|
||||
config_vbox = self.xml.get_widget('config_vbox')
|
||||
dataform = dataforms.ExtendForm(node = self.form)
|
||||
self.data_form_widget = dataforms_widget.DataFormWidget(dataform)
|
||||
self.data_form_widget.show()
|
||||
config_vbox.pack_start(self.data_form_widget)
|
||||
|
||||
self.xml.signal_autoconnect(self)
|
||||
self.window.show_all()
|
||||
|
||||
def on_ok_button_clicked(self, widget):
|
||||
acceptance = xmpp.Message(self.jid)
|
||||
acceptance.setThread(self.session.thread_id)
|
||||
feature = acceptance.NT.feature
|
||||
feature.setNamespace(xmpp.NS_FEATURE)
|
||||
|
||||
form = self.data_form_widget.data_form
|
||||
form.setAttr('type', 'submit')
|
||||
|
||||
feature.addChild(node=form)
|
||||
|
||||
gajim.connections[self.account].send_stanza(acceptance)
|
||||
|
||||
self.window.destroy()
|
||||
|
||||
def on_cancel_button_clicked(self, widget):
|
||||
# XXX determine whether to reveal presence
|
||||
|
||||
rejection = xmpp.Message(self.jid)
|
||||
rejection.setThread(self.session.thread_id)
|
||||
feature = rejection.NT.feature
|
||||
feature.setNamespace(xmpp.NS_FEATURE)
|
||||
|
||||
x = xmpp.DataForm(typ='submit')
|
||||
x.addChild(node=xmpp.DataField('FORM_TYPE', value='urn:xmpp:ssn'))
|
||||
x.addChild(node=xmpp.DataField('accept', value='false', typ='boolean'))
|
||||
|
||||
feature.addChild(node=x)
|
||||
|
||||
# XXX optional <body/>
|
||||
|
||||
gajim.connections[self.account].send_stanza(rejection)
|
||||
|
||||
self.window.destroy()
|
|
@ -1230,10 +1230,15 @@ class RosterWindow:
|
|||
'''reads from db the unread messages, and fire them up'''
|
||||
for jid in gajim.contacts.get_jid_list(account):
|
||||
results = gajim.logger.get_unread_msgs_for_jid(jid)
|
||||
|
||||
# XXX unread messages should probably have their session saved with them
|
||||
if results:
|
||||
session = gajim.connections[account].make_new_session(jid)
|
||||
|
||||
for result in results:
|
||||
tim = time.localtime(float(result[2]))
|
||||
self.on_message(jid, result[1], tim, account, msg_type = 'chat',
|
||||
msg_id = result[0])
|
||||
msg_id = result[0], session = session)
|
||||
|
||||
def fill_contacts_and_groups_dicts(self, array, account):
|
||||
'''fill gajim.contacts and gajim.groups'''
|
||||
|
@ -3758,7 +3763,7 @@ class RosterWindow:
|
|||
self.actions_menu_needs_rebuild = True
|
||||
self.update_status_combobox()
|
||||
|
||||
def new_private_chat(self, gc_contact, account):
|
||||
def new_private_chat(self, gc_contact, account, session = None):
|
||||
contact = gajim.contacts.contact_from_gc_contact(gc_contact)
|
||||
type_ = message_control.TYPE_PM
|
||||
fjid = gc_contact.room_jid + '/' + gc_contact.name
|
||||
|
@ -3766,24 +3771,25 @@ class RosterWindow:
|
|||
if not mw:
|
||||
mw = gajim.interface.msg_win_mgr.create_window(contact, account, type_)
|
||||
|
||||
chat_control = PrivateChatControl(mw, gc_contact, contact, account)
|
||||
chat_control = PrivateChatControl(mw, gc_contact, contact, account, session)
|
||||
mw.new_tab(chat_control)
|
||||
if len(gajim.events.get_events(account, fjid)):
|
||||
# We call this here to avoid race conditions with widget validation
|
||||
chat_control.read_queue()
|
||||
|
||||
def new_chat(self, contact, account, resource = None):
|
||||
def new_chat(self, contact, account, resource = None, session = None):
|
||||
# Get target window, create a control, and associate it with the window
|
||||
type_ = message_control.TYPE_CHAT
|
||||
|
||||
fjid = contact.jid
|
||||
if resource:
|
||||
fjid += '/' + resource
|
||||
|
||||
mw = gajim.interface.msg_win_mgr.get_window(fjid, account)
|
||||
if not mw:
|
||||
mw = gajim.interface.msg_win_mgr.create_window(contact, account, type_)
|
||||
|
||||
chat_control = ChatControl(mw, contact, account, resource)
|
||||
chat_control = ChatControl(mw, contact, account, session, resource)
|
||||
|
||||
mw.new_tab(chat_control)
|
||||
|
||||
|
@ -3827,7 +3833,7 @@ class RosterWindow:
|
|||
|
||||
def on_message(self, jid, msg, tim, account, encrypted = False,
|
||||
msg_type = '', subject = None, resource = '', msg_id = None,
|
||||
user_nick = '', advanced_notif_num = None, xhtml = None):
|
||||
user_nick = '', advanced_notif_num = None, xhtml = None, session = None):
|
||||
'''when we receive a message'''
|
||||
contact = None
|
||||
# if chat window will be for specific resource
|
||||
|
@ -3884,7 +3890,7 @@ class RosterWindow:
|
|||
if msg_type == 'normal' and popup: # it's single message to be autopopuped
|
||||
dialogs.SingleMessageWindow(account, contact.jid,
|
||||
action = 'receive', from_whom = jid, subject = subject,
|
||||
message = msg, resource = resource)
|
||||
message = msg, resource = resource, session = session)
|
||||
return
|
||||
|
||||
# We print if window is opened and it's not a single message
|
||||
|
@ -3892,6 +3898,8 @@ class RosterWindow:
|
|||
typ = ''
|
||||
if msg_type == 'error':
|
||||
typ = 'status'
|
||||
if session:
|
||||
ctrl.set_session(session)
|
||||
ctrl.print_conversation(msg, typ, tim = tim, encrypted = encrypted,
|
||||
subject = subject, xhtml = xhtml)
|
||||
if msg_id:
|
||||
|
@ -3907,7 +3915,7 @@ class RosterWindow:
|
|||
show_in_roster = notify.get_show_in_roster(event_type, account, contact)
|
||||
show_in_systray = notify.get_show_in_systray(event_type, account, contact)
|
||||
event = gajim.events.create_event(type_, (msg, subject, msg_type, tim,
|
||||
encrypted, resource, msg_id, xhtml), show_in_roster = show_in_roster,
|
||||
encrypted, resource, msg_id, xhtml, session), show_in_roster = show_in_roster,
|
||||
show_in_systray = show_in_systray)
|
||||
gajim.events.add_event(account, fjid, event)
|
||||
if popup:
|
||||
|
@ -4171,7 +4179,7 @@ class RosterWindow:
|
|||
if event.type_ == 'normal':
|
||||
dialogs.SingleMessageWindow(account, jid,
|
||||
action = 'receive', from_whom = jid, subject = data[1],
|
||||
message = data[0], resource = data[5])
|
||||
message = data[0], resource = data[5], session = data[8])
|
||||
gajim.interface.remove_first_event(account, jid, event.type_)
|
||||
return True
|
||||
elif event.type_ == 'file-request':
|
||||
|
@ -4208,14 +4216,14 @@ class RosterWindow:
|
|||
jid = jid + u'/' + resource
|
||||
adhoc_commands.CommandWindow(account, jid)
|
||||
|
||||
def on_open_chat_window(self, widget, contact, account, resource = None):
|
||||
def on_open_chat_window(self, widget, contact, account, resource = None, session = None):
|
||||
# Get the window containing the chat
|
||||
fjid = contact.jid
|
||||
if resource:
|
||||
fjid += '/' + resource
|
||||
win = gajim.interface.msg_win_mgr.get_window(fjid, account)
|
||||
if not win:
|
||||
self.new_chat(contact, account, resource = resource)
|
||||
self.new_chat(contact, account, resource = resource, session = session)
|
||||
win = gajim.interface.msg_win_mgr.get_window(fjid, account)
|
||||
ctrl = win.get_control(fjid, account)
|
||||
# last message is long time ago
|
||||
|
@ -4262,7 +4270,9 @@ class RosterWindow:
|
|||
jid = child_jid
|
||||
else:
|
||||
child_iter = model.iter_next(child_iter)
|
||||
session = None
|
||||
if first_ev:
|
||||
session = first_ev.parameters[8]
|
||||
fjid = jid
|
||||
if resource:
|
||||
fjid += '/' + resource
|
||||
|
@ -4273,7 +4283,7 @@ class RosterWindow:
|
|||
c = gajim.contacts.get_contact_with_highest_priority(account, jid)
|
||||
if jid == gajim.get_jid_from_account(account):
|
||||
resource = c.resource
|
||||
self.on_open_chat_window(widget, c, account, resource = resource)
|
||||
self.on_open_chat_window(widget, c, account, resource = resource, session = session)
|
||||
|
||||
def on_roster_treeview_row_activated(self, widget, path, col = 0):
|
||||
'''When an iter is double clicked: open the first event window'''
|
||||
|
|
Loading…
Reference in New Issue