merge jingleFT branch! Thanks to Zhenchao Li and Jefry Lagrange for their work.

This commit is contained in:
Yann Leboulanger 2012-04-14 13:40:55 +02:00
commit 6d178205fd
26 changed files with 2970 additions and 594 deletions

View file

@ -53,7 +53,7 @@ from common.logger import constants
from common.pep import MOODS, ACTIVITIES from common.pep import MOODS, ACTIVITIES
from common.xmpp.protocol import NS_XHTML, NS_XHTML_IM, NS_FILE, NS_MUC from common.xmpp.protocol import NS_XHTML, NS_XHTML_IM, NS_FILE, NS_MUC
from common.xmpp.protocol import NS_RECEIPTS, NS_ESESSION from common.xmpp.protocol import NS_RECEIPTS, NS_ESESSION
from common.xmpp.protocol import NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO, NS_JINGLE_ICE_UDP from common.xmpp.protocol import NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO, NS_JINGLE_ICE_UDP, NS_JINGLE_FILE_TRANSFER
from common.xmpp.protocol import NS_CHATSTATES from common.xmpp.protocol import NS_CHATSTATES
from common.connection_handlers_events import MessageOutgoingEvent from common.connection_handlers_events import MessageOutgoingEvent
from common.exceptions import GajimGeneralException from common.exceptions import GajimGeneralException
@ -1722,13 +1722,13 @@ class ChatControl(ChatControlBase):
self._video_button.set_sensitive(self.video_available) self._video_button.set_sensitive(self.video_available)
# Send file # Send file
if self.contact.supports(NS_FILE) and (self.type_id == 'chat' or \ if (self.contact.supports(NS_FILE) or self.contact.supports(NS_JINGLE_FILE_TRANSFER)) and (self.type_id == 'chat' or \
self.gc_contact.resource): self.gc_contact.resource):
self._send_file_button.set_sensitive(True) self._send_file_button.set_sensitive(True)
self._send_file_button.set_tooltip_text('') self._send_file_button.set_tooltip_text('')
else: else:
self._send_file_button.set_sensitive(False) self._send_file_button.set_sensitive(False)
if not self.contact.supports(NS_FILE): if not (self.contact.supports(NS_FILE) or self.contact.supports(NS_JINGLE_FILE_TRANSFER)):
self._send_file_button.set_tooltip_text(_( self._send_file_button.set_tooltip_text(_(
"This contact does not support file transfer.")) "This contact does not support file transfer."))
else: else:

View file

@ -38,10 +38,10 @@ import logging
log = logging.getLogger('gajim.c.caps_cache') log = logging.getLogger('gajim.c.caps_cache')
from common.xmpp import (NS_XHTML_IM, NS_RECEIPTS, NS_ESESSION, NS_CHATSTATES, from common.xmpp import (NS_XHTML_IM, NS_RECEIPTS, NS_ESESSION, NS_CHATSTATES,
NS_JINGLE_ICE_UDP, NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO, NS_CAPS) NS_JINGLE_ICE_UDP, NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO, NS_CAPS, NS_JINGLE_FILE_TRANSFER)
# Features where we cannot safely assume that the other side supports them # Features where we cannot safely assume that the other side supports them
FEATURE_BLACKLIST = [NS_CHATSTATES, NS_XHTML_IM, NS_RECEIPTS, NS_ESESSION, FEATURE_BLACKLIST = [NS_CHATSTATES, NS_XHTML_IM, NS_RECEIPTS, NS_ESESSION,
NS_JINGLE_ICE_UDP, NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO] NS_JINGLE_ICE_UDP, NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO, NS_JINGLE_FILE_TRANSFER]
# Query entry status codes # Query entry status codes
NEW = 0 NEW = 0

View file

@ -30,6 +30,7 @@ import stat
from common import gajim from common import gajim
import logger import logger
from common import jingle_xtls
# DO NOT MOVE ABOVE OF import gajim # DO NOT MOVE ABOVE OF import gajim
import sqlite3 as sqlite import sqlite3 as sqlite
@ -266,6 +267,8 @@ def check_and_possibly_create_paths():
MY_DATA = configpaths.gajimpaths['MY_DATA'] MY_DATA = configpaths.gajimpaths['MY_DATA']
MY_CONFIG = configpaths.gajimpaths['MY_CONFIG'] MY_CONFIG = configpaths.gajimpaths['MY_CONFIG']
MY_CACHE = configpaths.gajimpaths['MY_CACHE'] MY_CACHE = configpaths.gajimpaths['MY_CACHE']
XTLS_CERTS = configpaths.gajimpaths['MY_PEER_CERTS']
LOCAL_XTLS_CERTS = configpaths.gajimpaths['MY_CERT']
PLUGINS_CONFIG_PATH = gajim.PLUGINS_CONFIG_DIR PLUGINS_CONFIG_PATH = gajim.PLUGINS_CONFIG_DIR
@ -342,6 +345,17 @@ def check_and_possibly_create_paths():
print _('%s is a directory but should be a file') % CACHE_DB_PATH print _('%s is a directory but should be a file') % CACHE_DB_PATH
print _('Gajim will now exit') print _('Gajim will now exit')
sys.exit() sys.exit()
if not os.path.exists(XTLS_CERTS):
create_path(XTLS_CERTS)
if not os.path.exists(LOCAL_XTLS_CERTS):
create_path(LOCAL_XTLS_CERTS)
cert_name = os.path.join(LOCAL_XTLS_CERTS,
jingle_xtls.SELF_SIGNED_CERTIFICATE)
if not (os.path.exists(cert_name + '.cert') and os.path.exists(
cert_name + '.pkey')):
jingle_xtls.make_certs(cert_name, 'gajim')
def create_path(directory): def create_path(directory):
head, tail = os.path.split(directory) head, tail = os.path.split(directory)

View file

@ -141,7 +141,7 @@ class ConfigPaths:
d = {'MY_DATA': '', 'LOG_DB': u'logs.db', 'MY_CACERTS': u'cacerts.pem', d = {'MY_DATA': '', 'LOG_DB': u'logs.db', 'MY_CACERTS': u'cacerts.pem',
'MY_EMOTS': u'emoticons', 'MY_ICONSETS': u'iconsets', 'MY_EMOTS': u'emoticons', 'MY_ICONSETS': u'iconsets',
'MY_MOOD_ICONSETS': u'moods', 'MY_ACTIVITY_ICONSETS': u'activities', 'MY_MOOD_ICONSETS': u'moods', 'MY_ACTIVITY_ICONSETS': u'activities',
'PLUGINS_USER': u'plugins'} 'PLUGINS_USER': u'plugins', 'MY_PEER_CERTS': u'certs'}
for name in d: for name in d:
self.add(name, TYPE_DATA, windowsify(d[name])) self.add(name, TYPE_DATA, windowsify(d[name]))
@ -151,6 +151,7 @@ class ConfigPaths:
self.add(name, TYPE_CACHE, windowsify(d[name])) self.add(name, TYPE_CACHE, windowsify(d[name]))
self.add('MY_CONFIG', TYPE_CONFIG, '') self.add('MY_CONFIG', TYPE_CONFIG, '')
self.add('MY_CERT', TYPE_CONFIG, '')
basedir = fse(os.environ.get(u'GAJIM_BASEDIR', defs.basedir)) basedir = fse(os.environ.get(u'GAJIM_BASEDIR', defs.basedir))
self.add('DATA', None, os.path.join(basedir, windowsify(u'data'))) self.add('DATA', None, os.path.join(basedir, windowsify(u'data')))

View file

@ -45,6 +45,7 @@ from common import helpers
from common import gajim from common import gajim
from common import exceptions from common import exceptions
from common import dataforms from common import dataforms
from common import jingle_xtls
from common.commands import ConnectionCommands from common.commands import ConnectionCommands
from common.pubsub import ConnectionPubSub from common.pubsub import ConnectionPubSub
from common.pep import ConnectionPEP from common.pep import ConnectionPEP
@ -189,7 +190,10 @@ class ConnectionDisco:
query.setAttr('node', 'http://gajim.org#' + gajim.version.split('-', 1)[ query.setAttr('node', 'http://gajim.org#' + gajim.version.split('-', 1)[
0]) 0])
for f in (common.xmpp.NS_BYTESTREAM, common.xmpp.NS_SI, for f in (common.xmpp.NS_BYTESTREAM, common.xmpp.NS_SI,
common.xmpp.NS_FILE, common.xmpp.NS_COMMANDS): common.xmpp.NS_FILE, common.xmpp.NS_COMMANDS,
common.xmpp.NS_JINGLE_FILE_TRANSFER, common.xmpp.NS_JINGLE_XTLS,
common.xmpp.NS_PUBKEY_PUBKEY, common.xmpp.NS_PUBKEY_REVOKE,
common.xmpp.NS_PUBKEY_ATTEST):
feature = common.xmpp.Node('feature') feature = common.xmpp.Node('feature')
feature.setAttr('var', f) feature.setAttr('var', f)
query.addChild(node=feature) query.addChild(node=feature)
@ -1979,10 +1983,37 @@ ConnectionJingle, ConnectionIBBytestream):
gajim.nec.push_incoming_event(SearchFormReceivedEvent(None, gajim.nec.push_incoming_event(SearchFormReceivedEvent(None,
conn=self, stanza=iq_obj)) conn=self, stanza=iq_obj))
def _StreamCB(self, con, iq_obj): def _search_fields_received(self, con, iq_obj):
log.debug('StreamCB') jid = jid = helpers.get_jid_from_iq(iq_obj)
gajim.nec.push_incoming_event(StreamReceivedEvent(None, tag = iq_obj.getTag('query', namespace = common.xmpp.NS_SEARCH)
conn=self, stanza=iq_obj)) if not tag:
self.dispatch('SEARCH_FORM', (jid, None, False))
return
df = tag.getTag('x', namespace = common.xmpp.NS_DATA)
if df:
self.dispatch('SEARCH_FORM', (jid, df, True))
return
df = {}
for i in iq_obj.getQueryPayload():
df[i.getName()] = i.getData()
self.dispatch('SEARCH_FORM', (jid, df, False))
def _PubkeyGetCB(self, con, iq_obj):
log.info('PubkeyGetCB')
jid_from = helpers.get_full_jid_from_iq(iq_obj)
sid = iq_obj.getAttr('id')
jingle_xtls.send_cert(con, jid_from, sid)
raise common.xmpp.NodeProcessed
def _PubkeyResultCB(self, con, iq_obj):
log.info('PubkeyResultCB')
jid_from = helpers.get_full_jid_from_iq(iq_obj)
jingle_xtls.handle_new_cert(con, iq_obj, jid_from)
def _StreamCB(self, con, obj):
if obj.getTag('conflict'):
# disconnected because of a resource conflict
self.dispatch('RESOURCE_CONFLICT', ())
def _register_handlers(self, con, con_type): def _register_handlers(self, con, con_type):
# try to find another way to register handlers in each class # try to find another way to register handlers in each class
@ -2070,5 +2101,7 @@ ConnectionJingle, ConnectionIBBytestream):
con.RegisterHandler('iq', self._ResultCB, 'result') con.RegisterHandler('iq', self._ResultCB, 'result')
con.RegisterHandler('presence', self._StanzaArrivedCB) con.RegisterHandler('presence', self._StanzaArrivedCB)
con.RegisterHandler('message', self._StanzaArrivedCB) con.RegisterHandler('message', self._StanzaArrivedCB)
con.RegisterHandler('unknown', self._StreamCB, con.RegisterHandler('unknown', self._StreamCB, 'urn:ietf:params:xml:ns:xmpp-streams', xmlns='http://etherx.jabber.org/streams')
common.xmpp.NS_XMPP_STREAMS, xmlns=common.xmpp.NS_STREAMS) con.RegisterHandler('iq', self._PubkeyGetCB, 'get', common.xmpp.NS_PUBKEY_PUBKEY)
con.RegisterHandler('iq', self._PubkeyResultCB, 'result', common.xmpp.NS_PUBKEY_PUBKEY)

View file

@ -36,6 +36,7 @@ from common.zeroconf import zeroconf
from common.logger import LOG_DB_PATH from common.logger import LOG_DB_PATH
from common.pep import SUPPORTED_PERSONAL_USER_EVENTS from common.pep import SUPPORTED_PERSONAL_USER_EVENTS
from common.xmpp.protocol import NS_CHATSTATES from common.xmpp.protocol import NS_CHATSTATES
from common.jingle_transport import JingleTransportSocks5
import gtkgui_helpers import gtkgui_helpers
@ -1392,6 +1393,16 @@ class JingleDisconnectedReceivedEvent(nec.NetworkIncomingEvent):
self.jid, self.resource = gajim.get_room_and_nick_from_fjid(self.fjid) self.jid, self.resource = gajim.get_room_and_nick_from_fjid(self.fjid)
self.sid = self.jingle_session.sid self.sid = self.jingle_session.sid
return True return True
class JingleTransferCancelledEvent(nec.NetworkIncomingEvent):
name = 'jingleFT-cancelled-received'
base_network_events = []
def generate(self):
self.fjid = self.jingle_session.peerjid
self.jid, self.resource = gajim.get_room_and_nick_from_fjid(self.fjid)
self.sid = self.jingle_session.sid
return True
class JingleErrorReceivedEvent(nec.NetworkIncomingEvent): class JingleErrorReceivedEvent(nec.NetworkIncomingEvent):
name = 'jingle-error-received' name = 'jingle-error-received'
@ -1901,6 +1912,10 @@ class FileRequestReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
name = 'file-request-received' name = 'file-request-received'
base_network_events = [] base_network_events = []
def init(self):
self.jingle_content = None
self.FT_content = None
def generate(self): def generate(self):
self.get_id() self.get_id()
self.fjid = self.conn._ft_get_from(self.stanza) self.fjid = self.conn._ft_get_from(self.stanza)
@ -1908,28 +1923,36 @@ class FileRequestReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
self.file_props = {'type': 'r'} self.file_props = {'type': 'r'}
self.file_props['sender'] = self.fjid self.file_props['sender'] = self.fjid
self.file_props['request-id'] = self.id_ self.file_props['request-id'] = self.id_
si = self.stanza.getTag('si') if self.jingle_content:
profile = si.getAttr('profile') self.file_props['session-type'] = 'jingle'
if profile != xmpp.NS_FILE: self.file_props['stream-methods'] = xmpp.NS_BYTESTREAM
self.conn.send_file_rejection(self.file_props, code='400', typ='profile') file_tag = self.jingle_content.getTag('description').getTag(
raise xmpp.NodeProcessed 'offer').getTag('file')
feature_tag = si.getTag('feature', namespace=xmpp.NS_FEATURE)
if not feature_tag:
return
form_tag = feature_tag.getTag('x', namespace=xmpp.NS_DATA)
if not form_tag:
return
self.dataform = dataforms.ExtendForm(node=form_tag)
for f in self.dataform.iter_fields():
if f.var == 'stream-method' and f.type == 'list-single':
values = [o[1] for o in f.options]
self.file_props['stream-methods'] = ' '.join(values)
if xmpp.NS_BYTESTREAM in values or xmpp.NS_IBB in values:
break
else: else:
self.conn.send_file_rejection(self.file_props, code='400', typ='stream') si = self.stanza.getTag('si')
raise xmpp.NodeProcessed profile = si.getAttr('profile')
file_tag = si.getTag('file') if profile != xmpp.NS_FILE:
self.conn.send_file_rejection(self.file_props, code='400',
typ='profile')
raise xmpp.NodeProcessed
feature_tag = si.getTag('feature', namespace=xmpp.NS_FEATURE)
if not feature_tag:
return
form_tag = feature_tag.getTag('x', namespace=xmpp.NS_DATA)
if not form_tag:
return
self.dataform = dataforms.ExtendForm(node=form_tag)
for f in self.dataform.iter_fields():
if f.var == 'stream-method' and f.type == 'list-single':
values = [o[1] for o in f.options]
self.file_props['stream-methods'] = ' '.join(values)
if xmpp.NS_BYTESTREAM in values or xmpp.NS_IBB in values:
break
else:
self.conn.send_file_rejection(self.file_props, code='400',
typ='stream')
raise xmpp.NodeProcessed
file_tag = si.getTag('file')
for attribute in file_tag.getAttrs(): for attribute in file_tag.getAttrs():
if attribute in ('name', 'size', 'hash', 'date'): if attribute in ('name', 'size', 'hash', 'date'):
val = file_tag.getAttr(attribute) val = file_tag.getAttr(attribute)
@ -1940,13 +1963,41 @@ class FileRequestReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
if file_desc_tag is not None: if file_desc_tag is not None:
self.file_props['desc'] = file_desc_tag.getData() self.file_props['desc'] = file_desc_tag.getData()
mime_type = si.getAttr('mime-type') if not self.jingle_content:
if mime_type is not None: mime_type = si.getAttr('mime-type')
self.file_props['mime-type'] = mime_type if mime_type is not None:
self.file_props['mime-type'] = mime_type
self.file_props['receiver'] = self.conn._ft_get_our_jid() self.file_props['receiver'] = self.conn._ft_get_our_jid()
self.file_props['sid'] = unicode(si.getAttr('id'))
self.file_props['transfered_size'] = [] self.file_props['transfered_size'] = []
if self.jingle_content:
self.FT_content.use_security = bool(self.jingle_content.getTag(
'security'))
self.file_props['session-sid'] = unicode(self.stanza.getTag(
'jingle').getAttr('sid'))
self.FT_content.file_props = self.file_props
if not self.FT_content.transport:
self.FT_content.transport = JingleTransportSocks5()
self.FT_content.transport.set_our_jid(
self.FT_content.session.ourjid)
self.FT_content.transport.set_connection(
self.FT_content.session.connection)
self.file_props['sid'] = self.FT_content.transport.sid
self.FT_content.session.connection.files_props[
self.file_props['sid']] = self.file_props
self.FT_content.transport.set_file_props(self.file_props)
if self.file_props.has_key('streamhosts'):
self.file_props['streamhosts'].extend(
self.FT_content.transport.remote_candidates)
else:
self.file_props['streamhosts'] = \
self.FT_content.transport.remote_candidates
for host in self.file_props['streamhosts']:
host['initiator'] = self.FT_content.session.initiator
host['target'] = self.FT_content.session.responder
else:
self.file_props['sid'] = unicode(si.getAttr('id'))
return True return True
class FileRequestErrorEvent(nec.NetworkIncomingEvent): class FileRequestErrorEvent(nec.NetworkIncomingEvent):

View file

@ -62,6 +62,7 @@ MY_ICONSETS_PATH = gajimpaths['MY_ICONSETS']
MY_MOOD_ICONSETS_PATH = gajimpaths['MY_MOOD_ICONSETS'] MY_MOOD_ICONSETS_PATH = gajimpaths['MY_MOOD_ICONSETS']
MY_ACTIVITY_ICONSETS_PATH = gajimpaths['MY_ACTIVITY_ICONSETS'] MY_ACTIVITY_ICONSETS_PATH = gajimpaths['MY_ACTIVITY_ICONSETS']
MY_CACERTS = gajimpaths['MY_CACERTS'] MY_CACERTS = gajimpaths['MY_CACERTS']
MY_PEER_CERTS_PATH = gajimpaths['MY_PEER_CERTS']
TMP = gajimpaths['TMP'] TMP = gajimpaths['TMP']
DATA_DIR = gajimpaths['DATA'] DATA_DIR = gajimpaths['DATA']
ICONS_DIR = gajimpaths['ICONS'] ICONS_DIR = gajimpaths['ICONS']
@ -69,6 +70,7 @@ HOME_DIR = gajimpaths['HOME']
PLUGINS_DIRS = [gajimpaths['PLUGINS_BASE'], PLUGINS_DIRS = [gajimpaths['PLUGINS_BASE'],
gajimpaths['PLUGINS_USER']] gajimpaths['PLUGINS_USER']]
PLUGINS_CONFIG_DIR = gajimpaths['PLUGINS_CONFIG_DIR'] PLUGINS_CONFIG_DIR = gajimpaths['PLUGINS_CONFIG_DIR']
MY_CERT_DIR = gajimpaths['MY_CERT']
try: try:
LANG = locale.getdefaultlocale()[0] # en_US, fr_FR, el_GR etc.. LANG = locale.getdefaultlocale()[0] # en_US, fr_FR, el_GR etc..
@ -206,7 +208,9 @@ gajim_common_features = [xmpp.NS_BYTESTREAM, xmpp.NS_SI, xmpp.NS_FILE,
'jabber:iq:gateway', xmpp.NS_LAST, xmpp.NS_PRIVACY, xmpp.NS_PRIVATE, 'jabber:iq:gateway', xmpp.NS_LAST, xmpp.NS_PRIVACY, xmpp.NS_PRIVATE,
xmpp.NS_REGISTER, xmpp.NS_VERSION, xmpp.NS_DATA, xmpp.NS_ENCRYPTED, 'msglog', xmpp.NS_REGISTER, xmpp.NS_VERSION, xmpp.NS_DATA, xmpp.NS_ENCRYPTED, 'msglog',
'sslc2s', 'stringprep', xmpp.NS_PING, xmpp.NS_TIME_REVISED, xmpp.NS_SSN, 'sslc2s', 'stringprep', xmpp.NS_PING, xmpp.NS_TIME_REVISED, xmpp.NS_SSN,
xmpp.NS_MOOD, xmpp.NS_ACTIVITY, xmpp.NS_NICK, xmpp.NS_ROSTERX, xmpp.NS_SECLABEL] xmpp.NS_MOOD, xmpp.NS_ACTIVITY, xmpp.NS_NICK, xmpp.NS_ROSTERX, xmpp.NS_SECLABEL,
xmpp.NS_HASHES, xmpp.NS_HASHES_MD5, xmpp.NS_HASHES_SHA1,
xmpp.NS_HASHES_SHA256, xmpp.NS_HASHES_SHA512]
# Optional features gajim supports per account # Optional features gajim supports per account
gajim_optional_features = {} gajim_optional_features = {}

View file

@ -1336,6 +1336,10 @@ def update_optional_features(account = None):
gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_RTP_AUDIO) gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_RTP_AUDIO)
gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_RTP_VIDEO) gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_RTP_VIDEO)
gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_ICE_UDP) gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_ICE_UDP)
gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_FILE_TRANSFER)
gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_XTLS)
gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_BYTESTREAM)
gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_IBB)
gajim.caps_hash[a] = caps_cache.compute_caps_hash([gajim.gajim_identity], gajim.caps_hash[a] = caps_cache.compute_caps_hash([gajim.gajim_identity],
gajim.gajim_common_features + gajim.gajim_optional_features[a]) gajim.gajim_common_features + gajim.gajim_optional_features[a])
# re-send presence with new hash # re-send presence with new hash

View file

@ -37,7 +37,11 @@ import gajim
from jingle_session import JingleSession, JingleStates from jingle_session import JingleSession, JingleStates
if gajim.HAVE_FARSTREAM: if gajim.HAVE_FARSTREAM:
from jingle_rtp import JingleAudio, JingleVideo from jingle_rtp import JingleAudio, JingleVideo
from jingle_ft import JingleFileTransfer
from jingle_transport import JingleTransportSocks5, JingleTransportIBB
import logging
logger = logging.getLogger('gajim.c.jingle')
class ConnectionJingle(object): class ConnectionJingle(object):
""" """
@ -75,27 +79,38 @@ class ConnectionJingle(object):
""" """
# get data # get data
jid = helpers.get_full_jid_from_iq(stanza) jid = helpers.get_full_jid_from_iq(stanza)
id = stanza.getID() id_ = stanza.getID()
if (jid, id) in self.__iq_responses.keys(): if (jid, id_) in self.__iq_responses.keys():
self.__iq_responses[(jid, id)].on_stanza(stanza) self.__iq_responses[(jid, id_)].on_stanza(stanza)
del self.__iq_responses[(jid, id)] del self.__iq_responses[(jid, id_)]
raise xmpp.NodeProcessed raise xmpp.NodeProcessed
jingle = stanza.getTag('jingle') jingle = stanza.getTag('jingle')
if not jingle: return # a jingle element is not necessary in iq-result stanza
sid = jingle.getAttr('sid') # don't check for that
if jingle:
sid = jingle.getAttr('sid')
else:
sid = None
for sesn in self._sessions.values():
if id_ in sesn.iq_ids:
sesn.on_stanza(stanza)
return
# do we need to create a new jingle object # do we need to create a new jingle object
if sid not in self._sessions: if sid not in self._sessions:
#TODO: tie-breaking and other things... #TODO: tie-breaking and other things...
newjingle = JingleSession(con=self, weinitiate=False, jid=jid, sid=sid) newjingle = JingleSession(con=self, weinitiate=False, jid=jid,
iq_id=id_, sid=sid)
self._sessions[sid] = newjingle self._sessions[sid] = newjingle
# we already have such session in dispatcher... # we already have such session in dispatcher...
self._sessions[sid].collect_iq_id(id_)
self._sessions[sid].on_stanza(stanza) self._sessions[sid].on_stanza(stanza)
# Delete invalid/unneeded sessions # Delete invalid/unneeded sessions
if sid in self._sessions and self._sessions[sid].state == JingleStates.ended: if sid in self._sessions and \
self._sessions[sid].state == JingleStates.ended:
self.delete_jingle_session(sid) self.delete_jingle_session(sid)
raise xmpp.NodeProcessed raise xmpp.NodeProcessed
@ -126,16 +141,55 @@ class ConnectionJingle(object):
jingle.start_session() jingle.start_session()
return jingle.sid return jingle.sid
def start_file_transfer(self, jid, file_props):
logger.info("start file transfer with file: %s" % file_props)
contact = gajim.contacts.get_contact_with_highest_priority(self.name,
gajim.get_jid_without_resource(jid))
if contact is None:
return
use_security = contact.supports(xmpp.NS_JINGLE_XTLS)
jingle = JingleSession(self, weinitiate=True, jid=jid)
# this is a file transfer
jingle.session_type_FT = True
self._sessions[jingle.sid] = jingle
file_props['sid'] = jingle.sid
if contact.supports(xmpp.NS_JINGLE_BYTESTREAM):
transport = JingleTransportSocks5()
elif contact.supports(xmpp.NS_JINGLE_IBB):
transport = JingleTransportIBB()
c = JingleFileTransfer(jingle, transport=transport,
file_props=file_props, use_security=use_security)
jingle.hash_algo = self.__hash_support(contact)
jingle.add_content('file' + helpers.get_random_string_16(), c)
jingle.start_session()
return c.transport.sid
def __hash_support(self, contact):
if contact.supports(xmpp.NS_HASHES):
if contact.supports(xmpp.NS_HASHES_MD5):
return 'md5'
elif contact.supports(xmpp.NS_HASHES_SHA1):
return 'sha-1'
elif contact.supports(xmpp.NS_HASHES_SHA256):
return 'sha-256'
elif contact.supports(xmpp.NS_HASHES_SHA512):
return 'sha-512'
return None
def iter_jingle_sessions(self, jid, sid=None, media=None): def iter_jingle_sessions(self, jid, sid=None, media=None):
if sid: if sid:
return (session for session in self._sessions.values() if session.sid == sid) return (session for session in self._sessions.values() if \
sessions = (session for session in self._sessions.values() if session.peerjid == jid) session.sid == sid)
sessions = (session for session in self._sessions.values() if \
session.peerjid == jid)
if media: if media:
if media not in ('audio', 'video'): if media not in ('audio', 'video', 'file'):
return tuple() return tuple()
else: else:
return (session for session in sessions if session.get_content(media)) return (session for session in sessions if \
session.get_content(media))
else: else:
return sessions return sessions
@ -147,6 +201,8 @@ class ConnectionJingle(object):
else: else:
return None return None
elif media: elif media:
if media not in ('audio', 'video', 'file'):
return None
for session in self._sessions.values(): for session in self._sessions.values():
if session.peerjid == jid and session.get_content(media): if session.peerjid == jid and session.get_content(media):
return session return session

View file

@ -16,6 +16,7 @@ Handles Jingle contents (XEP 0166)
""" """
import xmpp import xmpp
from jingle_transport import JingleTransportIBB
contents = {} contents = {}
@ -69,7 +70,7 @@ class JingleContent(object):
'session-initiate': [self.__on_transport_info], 'session-initiate': [self.__on_transport_info],
'session-terminate': [], 'session-terminate': [],
'transport-info': [self.__on_transport_info], 'transport-info': [self.__on_transport_info],
'transport-replace': [], 'transport-replace': [self.__on_transport_replace],
'transport-accept': [], 'transport-accept': [],
'transport-reject': [], 'transport-reject': [],
'iq-result': [], 'iq-result': [],
@ -99,7 +100,7 @@ class JingleContent(object):
""" """
Add a list of candidates to the list of remote candidates Add a list of candidates to the list of remote candidates
""" """
pass self.transport.remote_candidates = candidates
def on_stanza(self, stanza, content, error, action): def on_stanza(self, stanza, content, error, action):
""" """
@ -109,12 +110,15 @@ class JingleContent(object):
for callback in self.callbacks[action]: for callback in self.callbacks[action]:
callback(stanza, content, error, action) callback(stanza, content, error, action)
def __on_transport_replace(self, stanza, content, error, action):
content.addChild(node=self.transport.make_transport())
def __on_transport_info(self, stanza, content, error, action): def __on_transport_info(self, stanza, content, error, action):
""" """
Got a new transport candidate Got a new transport candidate
""" """
candidates = self.transport.parse_transport_stanza( candidates = self.transport.parse_transport_stanza(
content.getTag('transport')) content.getTag('transport'))
if candidates: if candidates:
self.add_remote_candidates(candidates) self.add_remote_candidates(candidates)
@ -134,6 +138,17 @@ class JingleContent(object):
content.addChild(node=self.transport.make_transport([candidate])) content.addChild(node=self.transport.make_transport([candidate]))
self.session.send_transport_info(content) self.session.send_transport_info(content)
def send_error_candidate(self):
"""
Sends a candidate-error when we can't connect to a candidate.
"""
content = self.__content()
tp = self.transport.make_transport(add_candidates=False)
tp.addChild(name='candidate-error')
content.addChild(node=tp)
self.session.send_transport_info(content)
def send_description_info(self): def send_description_info(self):
content = self.__content() content = self.__content()
self._fill_content(content) self._fill_content(content)

375
src/common/jingle_ft.py Normal file
View file

@ -0,0 +1,375 @@
# -*- coding:utf-8 -*-
## This file is part of Gajim.
##
## Gajim is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
## by the Free Software Foundation; version 3 only.
##
## Gajim is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
##
"""
Handles Jingle File Transfer (XEP 0234)
"""
import gajim
import xmpp
from jingle_content import contents, JingleContent
from jingle_transport import *
from common import helpers
from common.socks5 import Socks5ReceiverClient, Socks5SenderClient
from common.connection_handlers_events import FileRequestReceivedEvent
import threading
import logging
from jingle_ftstates import *
log = logging.getLogger('gajim.c.jingle_ft')
STATE_NOT_STARTED = 0
STATE_INITIALIZED = 1
# We send the candidates and we are waiting for a reply
STATE_CAND_SENT = 2
# We received the candidates and we are waiting to reply
STATE_CAND_RECEIVED = 3
# We have sent and received the candidates
# This also includes any candidate-error received or sent
STATE_CAND_SENT_AND_RECEIVED = 4
STATE_TRANSPORT_REPLACE = 5
# We are transfering the file
STATE_TRANSFERING = 6
class JingleFileTransfer(JingleContent):
def __init__(self, session, transport=None, file_props=None,
use_security=False):
JingleContent.__init__(self, session, transport)
log.info("transport value: %s" % transport)
# events we might be interested in
self.callbacks['session-initiate'] += [self.__on_session_initiate]
self.callbacks['session-initiate-sent'] += [self.__on_session_initiate_sent]
self.callbacks['content-add'] += [self.__on_session_initiate]
self.callbacks['session-accept'] += [self.__on_session_accept]
self.callbacks['session-terminate'] += [self.__on_session_terminate]
self.callbacks['session-info'] += [self.__on_session_info]
self.callbacks['transport-accept'] += [self.__on_transport_accept]
self.callbacks['transport-replace'] += [self.__on_transport_replace]
self.callbacks['session-accept-sent'] += [self.__transport_setup]
# fallback transport method
self.callbacks['transport-reject'] += [self.__on_transport_reject]
self.callbacks['transport-info'] += [self.__on_transport_info]
self.callbacks['iq-result'] += [self.__on_iq_result]
self.use_security = use_security
self.file_props = file_props
if file_props is None:
self.weinitiate = False
else:
self.weinitiate = True
if self.file_props is not None:
self.file_props['sender'] = session.ourjid
self.file_props['receiver'] = session.peerjid
self.file_props['session-type'] = 'jingle'
self.file_props['session-sid'] = session.sid
self.file_props['transfered_size'] = []
log.info("FT request: %s" % file_props)
if transport is None:
self.transport = JingleTransportSocks5()
self.transport.set_connection(session.connection)
self.transport.set_file_props(self.file_props)
self.transport.set_our_jid(session.ourjid)
log.info('ourjid: %s' % session.ourjid)
if self.file_props is not None:
self.file_props['sid'] = self.transport.sid
self.session = session
self.media = 'file'
self.nominated_cand = {}
self.state = STATE_NOT_STARTED
self.states = {STATE_INITIALIZED : StateInitialized(self),
STATE_CAND_SENT : StateCandSent(self),
STATE_CAND_RECEIVED : StateCandReceived(self),
STATE_TRANSFERING : StateTransfering(self),
STATE_TRANSPORT_REPLACE : StateTransportReplace(self),
STATE_CAND_SENT_AND_RECEIVED : StateCandSentAndRecv(self)
}
def __state_changed(self, nextstate, args=None):
# Executes the next state action and sets the next state
st = self.states[nextstate]
st.action(args)
self.state = nextstate
def __on_session_initiate(self, stanza, content, error, action):
gajim.nec.push_incoming_event(FileRequestReceivedEvent(None,
conn=self.session.connection, stanza=stanza, jingle_content=content,
FT_content=self))
def __on_session_initiate_sent(self, stanza, content, error, action):
# Calculate file_hash in a new thread
self.hashThread = threading.Thread(target=self.__calcHash)
self.hashThread.start()
def __calcHash(self):
if self.session.hash_algo == None:
return
try:
file_ = open(self.file_props['file-name'], 'r')
except:
# can't open file
return
h = xmpp.Hashes()
hash_ = h.calculateHash(self.session.hash_algo, file_)
if not hash_:
# Hash alogrithm not supported
return
self.file_props['hash'] = hash_
h.addHash(hash_, self.session.hash_algo)
checksum = xmpp.Node(tag='checksum',
payload=[xmpp.Node(tag='file', payload=[h])])
checksum.setNamespace(xmpp.NS_JINGLE_FILE_TRANSFER)
# Send hash in a session info
self.session.__session_info(checksum )
def __on_session_accept(self, stanza, content, error, action):
log.info("__on_session_accept")
con = self.session.connection
security = content.getTag('security')
if not security: # responder can not verify our fingerprint
self.use_security = False
if self.state == STATE_TRANSPORT_REPLACE:
# We ack the session accept
response = stanza.buildReply('result')
response.delChild(response.getQuery())
con.connection.send(response)
# We send the file
self.__state_changed(STATE_TRANSFERING)
raise xmpp.NodeProcessed
self.file_props['streamhosts'] = self.transport.remote_candidates
for host in self.file_props['streamhosts']:
host['initiator'] = self.session.initiator
host['target'] = self.session.responder
host['sid'] = self.file_props['sid']
response = stanza.buildReply('result')
response.delChild(response.getQuery())
con.connection.send(response)
if not gajim.socks5queue.get_file_props(
self.session.connection.name, self.file_props['sid']):
gajim.socks5queue.add_file_props(self.session.connection.name,
self.file_props)
fingerprint = None
if self.use_security:
fingerprint = 'client'
if self.transport.type == TransportType.SOCKS5:
gajim.socks5queue.connect_to_hosts(self.session.connection.name,
self.file_props['sid'], self.on_connect,
self._on_connect_error, fingerprint=fingerprint,
receiving=False)
return
self.__state_changed(STATE_TRANSFERING)
raise xmpp.NodeProcessed
def __on_session_terminate(self, stanza, content, error, action):
log.info("__on_session_terminate")
def __on_session_info(self, stanza, content, error, action):
pass
def __on_transport_accept(self, stanza, content, error, action):
log.info("__on_transport_accept")
def __on_transport_replace(self, stanza, content, error, action):
log.info("__on_transport_replace")
def __on_transport_reject(self, stanza, content, error, action):
log.info("__on_transport_reject")
def __on_transport_info(self, stanza, content, error, action):
log.info("__on_transport_info")
if content.getTag('transport').getTag('candidate-error'):
self.nominated_cand['peer-cand'] = False
if self.state == STATE_CAND_SENT:
if not self.nominated_cand['our-cand'] and \
not self.nominated_cand['peer-cand']:
if not self.weinitiate:
return
self.__state_changed(STATE_TRANSPORT_REPLACE)
else:
response = stanza.buildReply('result')
response.delChild(response.getQuery())
self.session.connection.connection.send(response)
self.__state_changed(STATE_TRANSFERING)
raise xmpp.NodeProcessed
else:
args = {'candError' : True}
self.__state_changed(STATE_CAND_RECEIVED, args)
return
if content.getTag('transport').getTag('activated'):
self.state = STATE_TRANSFERING
jid = gajim.get_jid_without_resource(self.session.ourjid)
gajim.socks5queue.send_file(self.file_props,
self.session.connection.name, 'client')
return
args = {'content' : content,
'sendCand' : False}
if self.state == STATE_CAND_SENT:
self.__state_changed(STATE_CAND_SENT_AND_RECEIVED, args)
response = stanza.buildReply('result')
response.delChild(response.getQuery())
self.session.connection.connection.send(response)
self.__state_changed(STATE_TRANSFERING)
raise xmpp.NodeProcessed
else:
self.__state_changed(STATE_CAND_RECEIVED, args)
def __on_iq_result(self, stanza, content, error, action):
log.info("__on_iq_result")
if self.state == STATE_NOT_STARTED:
self.__state_changed(STATE_INITIALIZED)
elif self.state == STATE_CAND_SENT_AND_RECEIVED:
if not self.nominated_cand['our-cand'] and \
not self.nominated_cand['peer-cand']:
if not self.weinitiate:
return
self.__state_changed(STATE_TRANSPORT_REPLACE)
return
# initiate transfer
self.__state_changed(STATE_TRANSFERING)
def __transport_setup(self, stanza=None, content=None, error=None,
action=None):
# Sets up a few transport specific things for the file transfer
if self.transport.type == TransportType.IBB:
# No action required, just set the state to transfering
self.state = STATE_TRANSFERING
def on_connect(self, streamhost):
"""
send candidate-used stanza
"""
log.info('send_candidate_used')
if streamhost is None:
return
args = {'streamhost' : streamhost,
'sendCand' : True}
self.nominated_cand['our-cand'] = streamhost
self.__sendCand(args)
def _on_connect_error(self, sid):
log.info('connect error, sid=' + sid)
args = {'candError' : True}
self.__sendCand(args)
def __sendCand(self, args):
if self.state == STATE_CAND_RECEIVED:
self.__state_changed(STATE_CAND_SENT_AND_RECEIVED, args)
else:
self.__state_changed(STATE_CAND_SENT, args)
def _fill_content(self, content):
description_node = xmpp.simplexml.Node(
tag=xmpp.NS_JINGLE_FILE_TRANSFER + ' description')
sioffer = xmpp.simplexml.Node(tag='offer')
file_tag = sioffer.setTag('file', namespace=xmpp.NS_FILE)
file_tag.setAttr('name', self.file_props['name'])
file_tag.setAttr('size', self.file_props['size'])
desc = file_tag.setTag('desc')
if 'desc' in self.file_props:
desc.setData(self.file_props['desc'])
description_node.addChild(node=sioffer)
if self.use_security:
security = xmpp.simplexml.Node(
tag=xmpp.NS_JINGLE_XTLS + ' security')
# TODO: add fingerprint element
for m in ('x509', ): # supported authentication methods
method = xmpp.simplexml.Node(tag='method')
method.setAttr('name', m)
security.addChild(node=method)
content.addChild(node=security)
content.addChild(node=description_node)
def _store_socks5_sid(self, sid, hash_id):
# callback from socsk5queue.start_listener
self.file_props['hash'] = hash_id
def _listen_host(self):
receiver = self.file_props['receiver']
sender = self.file_props['sender']
sha_str = helpers.get_auth_sha(self.file_props['sid'], sender,
receiver)
self.file_props['sha_str'] = sha_str
port = gajim.config.get('file_transfers_port')
fingerprint = None
if self.use_security:
fingerprint = 'server'
if self.weinitiate:
listener = gajim.socks5queue.start_listener(port, sha_str,
self._store_socks5_sid, self.file_props,
fingerprint=fingerprint, type='sender')
else:
listener = gajim.socks5queue.start_listener(port, sha_str,
self._store_socks5_sid, self.file_props,
fingerprint=fingerprint, type='receiver')
if not listener:
# send error message, notify the user
return
def isOurCandUsed(self):
'''
If this method returns true then the candidate we nominated will be
used, if false, the candidate nominated by peer will be used
'''
if self.nominated_cand['peer-cand'] == False:
return True
if self.nominated_cand['our-cand'] == False:
return False
peer_pr = int(self.nominated_cand['peer-cand']['priority'])
our_pr = int(self.nominated_cand['our-cand']['priority'])
if peer_pr != our_pr:
return our_pr > peer_pr
else:
return self.weinitiate
def get_content(desc):
return JingleFileTransfer
contents[xmpp.NS_JINGLE_FILE_TRANSFER] = get_content

View file

@ -0,0 +1,244 @@
##
## Copyright (C) 2006 Gajim Team
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
## by the Free Software Foundation; version 2 only.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
import gajim
import xmpp
from jingle_transport import *
class JingleFileTransferStates:
# This class implements the state machine design pattern
def __init__(self, jingleft):
self.jft = jingleft
def action(self, args=None):
'''
This method MUST be overriden by a subclass
'''
raise Exception('This is an abstract method!!')
class StateInitialized(JingleFileTransferStates):
'''
This state initializes the file transfer
'''
def action(self, args=None):
self.jft._listen_host()
if self.jft.weinitiate:
# update connection's fileprops
self.jft.session.connection.files_props[self.jft.file_props['sid']] = \
self.jft.file_props
# Listen on configured port for file transfer
else:
# Add file_props to the queue
if not gajim.socks5queue.get_file_props(
self.jft.session.connection.name, self.jft.file_props['sid']):
gajim.socks5queue.add_file_props(
self.jft.session.connection.name,
self.jft.file_props)
fingerprint = None
if self.jft.use_security:
fingerprint = 'client'
# Connect to the candidate host, on success call on_connect method
gajim.socks5queue.connect_to_hosts(
self.jft.session.connection.name,
self.jft.file_props['sid'], self.jft.on_connect,
self.jft._on_connect_error, fingerprint=fingerprint)
class StateCandSent(JingleFileTransferStates):
'''
This state sends our nominated candidate
'''
def _sendCand(self, args):
if 'candError' in args:
self.jft.nominated_cand['our-cand'] = False
self.jft.send_error_candidate()
return
# Send candidate used
streamhost = args['streamhost']
self.jft.nominated_cand['our-cand'] = streamhost
content = xmpp.Node('content')
content.setAttr('creator', 'initiator')
content.setAttr('name', self.jft.name)
transport = xmpp.Node('transport')
transport.setNamespace(xmpp.NS_JINGLE_BYTESTREAM)
transport.setAttr('sid', self.jft.transport.sid)
candidateused = xmpp.Node('candidate-used')
candidateused.setAttr('cid', streamhost['cid'])
transport.addChild(node=candidateused)
content.addChild(node=transport)
self.jft.session.send_transport_info(content)
def action(self, args=None):
self._sendCand(args)
class StateCandReceived(JingleFileTransferStates):
'''
This state happens when we receive a candidate.
It takes the arguments: canError if we receive a candidate-error
'''
def _recvCand(self, args):
if 'candError' in args:
return
content = args['content']
streamhost_cid = content.getTag('transport').getTag('candidate-used').\
getAttr('cid')
streamhost_used = None
for cand in self.jft.transport.candidates:
if cand['candidate_id'] == streamhost_cid:
streamhost_used = cand
break
if streamhost_used == None:
log.info("unknow streamhost")
return
# We save the candidate nominated by peer
self.jft.nominated_cand['peer-cand'] = streamhost_used
def action(self, args=None):
self._recvCand(args)
class StateCandSentAndRecv( StateCandSent, StateCandReceived):
'''
This state happens when we have received and sent the candidates.
It takes the boolean argument: sendCand in order to decide whether
we should execute the action of when we receive or send a candidate.
'''
def action(self, args=None):
if args['sendCand']:
self._sendCand(args)
else:
self._recvCand(args)
class StateTransportReplace(JingleFileTransferStates):
'''
This state initiates transport replace
'''
def action(self, args=None):
self.jft.session.transport_replace()
class StateTransfering(JingleFileTransferStates):
'''
This state will start the transfer depeding on the type of transport
we have.
'''
def __start_IBB_transfer(self, con):
con.files_props[self.jft.file_props['sid']] = \
self.jft.file_props
fp = open(self.jft.file_props['file-name'], 'r')
con.OpenStream( self.jft.transport.sid,
self.jft.session.peerjid, fp, blocksize=4096)
def __start_SOCK5_transfer(self):
# It tells wether we start the transfer as client or server
mode = None
if self.jft.isOurCandUsed():
mode = 'client'
streamhost_used = self.jft.nominated_cand['our-cand']
else:
mode = 'server'
streamhost_used = self.jft.nominated_cand['peer-cand']
if streamhost_used['type'] == 'proxy':
self.jft.file_props['is_a_proxy'] = True
# This needs to be changed when requesting
if self.jft.weinitiate:
self.jft.file_props['proxy_sender'] = streamhost_used['initiator']
self.jft.file_props['proxy_receiver'] = streamhost_used['target']
else:
self.jft.file_props['proxy_sender'] = streamhost_used['target']
self.jft.file_props['proxy_receiver'] = streamhost_used['initiator']
# This needs to be changed when requesting
if not self.jft.weinitiate and streamhost_used['type'] == 'proxy':
r = gajim.socks5queue.readers
for reader in r:
if r[reader].host == streamhost_used['host'] and \
r[reader].connected:
return
# This needs to be changed when requesting
if self.jft.weinitiate and streamhost_used['type'] == 'proxy':
s = gajim.socks5queue.senders
for sender in s:
if s[sender].host == streamhost_used['host'] and \
s[sender].connected:
return
if streamhost_used['type'] == 'proxy':
self.jft.file_props['streamhost-used'] = True
streamhost_used['sid'] = self.jft.file_props['sid']
self.jft.file_props['streamhosts'] = []
self.jft.file_props['streamhosts'].append(streamhost_used)
self.jft.file_props['proxyhosts'] = []
self.jft.file_props['proxyhosts'].append(streamhost_used)
# This needs to be changed when requesting
if self.jft.weinitiate:
gajim.socks5queue.idx += 1
idx = gajim.socks5queue.idx
sockobj = Socks5SenderClient(gajim.idlequeue, idx,
gajim.socks5queue, _sock=None,
host=str(streamhost_used['host']),
port=int(streamhost_used['port']), fingerprint=None,
connected=False, file_props=self.jft.file_props)
else:
sockobj = Socks5ReceiverClient(gajim.idlequeue, streamhost_used,
sid=self.jft.file_props['sid'],
file_props=self.jft.file_props, fingerprint=None)
sockobj.proxy = True
sockobj.streamhost = streamhost_used
gajim.socks5queue.add_sockobj(self.jft.session.connection.name,
sockobj, 'sender')
streamhost_used['idx'] = sockobj.queue_idx
# If we offered the nominated candidate used, we activate
# the proxy
if not self.jft.isOurCandUsed():
gajim.socks5queue.on_success[self.jft.file_props['sid']] = \
self.jft.transport._on_proxy_auth_ok
# TODO: add on failure
else:
jid = gajim.get_jid_without_resource(self.jft.session.ourjid)
gajim.socks5queue.send_file(self.jft.file_props,
self.jft.session.connection.name, mode)
def action(self, args=None):
if self.jft.transport.type == TransportType.IBB:
self.__start_IBB_transfer(self.jft.session.connection)
elif self.jft.transport.type == TransportType.SOCKS5:
self.__start_SOCK5_transfer()

View file

@ -38,7 +38,7 @@ log = logging.getLogger('gajim.c.jingle_rtp')
class JingleRTPContent(JingleContent): class JingleRTPContent(JingleContent):
def __init__(self, session, media, transport=None): def __init__(self, session, media, transport=None):
if transport is None: if transport is None:
transport = JingleTransportICEUDP() transport = JingleTransportICEUDP(None)
JingleContent.__init__(self, session, transport) JingleContent.__init__(self, session, transport)
self.media = media self.media = media
self._dtmf_running = False self._dtmf_running = False

View file

@ -28,9 +28,13 @@ Handles Jingle sessions (XEP 0166)
import gajim #Get rid of that? import gajim #Get rid of that?
import xmpp import xmpp
from jingle_transport import get_jingle_transport from jingle_transport import get_jingle_transport, JingleTransportIBB
from jingle_content import get_jingle_content, JingleContentSetupException from jingle_content import get_jingle_content, JingleContentSetupException
from jingle_content import JingleContent
from jingle_ft import STATE_TRANSPORT_REPLACE
from common.connection_handlers_events import * from common.connection_handlers_events import *
import logging
log = logging.getLogger("gajim.c.jingle_session")
# FIXME: Move it to JingleSession.States? # FIXME: Move it to JingleSession.States?
class JingleStates(object): class JingleStates(object):
@ -59,7 +63,7 @@ class JingleSession(object):
negotiated between an initiator and a responder. negotiated between an initiator and a responder.
""" """
def __init__(self, con, weinitiate, jid, sid=None): def __init__(self, con, weinitiate, jid, iq_id=None, sid=None):
""" """
con -- connection object, con -- connection object,
weinitiate -- boolean, are we the initiator? weinitiate -- boolean, are we the initiator?
@ -85,8 +89,21 @@ class JingleSession(object):
sid = con.connection.getAnID() sid = con.connection.getAnID()
self.sid = sid # sessionid self.sid = sid # sessionid
self.accepted = True # is this session accepted by user # iq stanza id, used to determine which sessions to summon callback
# later on when iq-result stanza arrives
if iq_id is not None:
self.iq_ids = [iq_id]
else:
self.iq_ids = []
self.accepted = True # is this session accepted by user
# Hash algorithm that we are using to calculate the integrity of the
# file. Could be 'md5', 'sha-1', etc...
self.hash_algo = None
self.file_hash = None
# Tells whether this session is a file transfer or not
self.session_type_FT = False
# callbacks to call on proper contents # callbacks to call on proper contents
# use .prepend() to add new callbacks, especially when you're going # use .prepend() to add new callbacks, especially when you're going
# to send error instead of ack # to send error instead of ack
@ -101,7 +118,7 @@ class JingleSession(object):
'description-info': [self.__broadcast, self.__ack], #TODO 'description-info': [self.__broadcast, self.__ack], #TODO
'security-info': [self.__ack], #TODO 'security-info': [self.__ack], #TODO
'session-accept': [self.__on_session_accept, self.__on_content_accept, 'session-accept': [self.__on_session_accept, self.__on_content_accept,
self.__broadcast, self.__ack], self.__broadcast],
'session-info': [self.__broadcast, self.__on_session_info, self.__ack], 'session-info': [self.__broadcast, self.__on_session_info, self.__ack],
'session-initiate': [self.__on_session_initiate, self.__broadcast, 'session-initiate': [self.__on_session_initiate, self.__broadcast,
self.__ack], self.__ack],
@ -111,9 +128,14 @@ class JingleSession(object):
'transport-replace': [self.__broadcast, self.__on_transport_replace], #TODO 'transport-replace': [self.__broadcast, self.__on_transport_replace], #TODO
'transport-accept': [self.__ack], #TODO 'transport-accept': [self.__ack], #TODO
'transport-reject': [self.__ack], #TODO 'transport-reject': [self.__ack], #TODO
'iq-result': [], 'iq-result': [self.__broadcast],
'iq-error': [self.__on_error], 'iq-error': [self.__on_error],
} }
def collect_iq_id(self, iq_id):
if iq_id is not None:
self.iq_ids.append(iq_id)
def approve_session(self): def approve_session(self):
""" """
@ -128,15 +150,23 @@ class JingleSession(object):
reason = xmpp.Node('reason') reason = xmpp.Node('reason')
reason.addChild('decline') reason.addChild('decline')
self._session_terminate(reason) self._session_terminate(reason)
def cancel_session(self):
"""
Called when user declines session in UI (when we aren't the initiator)
"""
reason = xmpp.Node('reason')
reason.addChild('cancel')
self._session_terminate(reason)
def approve_content(self, media): def approve_content(self, media, name=None):
content = self.get_content(media) content = self.get_content(media, name)
if content: if content:
content.accepted = True content.accepted = True
self.on_session_state_changed(content) self.on_session_state_changed(content)
def reject_content(self, media): def reject_content(self, media, name=None):
content = self.get_content(media) content = self.get_content(media, name)
if content: if content:
if self.state == JingleStates.active: if self.state == JingleStates.active:
self.__content_reject(content) self.__content_reject(content)
@ -154,13 +184,14 @@ class JingleSession(object):
reason.addChild('cancel') reason.addChild('cancel')
self._session_terminate(reason) self._session_terminate(reason)
def get_content(self, media=None): def get_content(self, media=None, name=None):
if media is None: if media is None:
return return
for content in self.contents.values(): for content in self.contents.values():
if content.media == media: if content.media == media:
return content if name is None or content.name == name:
return content
def add_content(self, name, content, creator='we'): def add_content(self, name, content, creator='we'):
""" """
@ -195,12 +226,22 @@ class JingleSession(object):
content = self.contents[(creator, name)] content = self.contents[(creator, name)]
self.__content_remove(content, reason) self.__content_remove(content, reason)
self.contents[(creator, name)].destroy() self.contents[(creator, name)].destroy()
if not self.contents:
self.end_session()
def modify_content(self, creator, name, *someother): def modify_content(self, creator, name, transport = None):
""" '''
We do not need this now Currently used for transport replacement
""" '''
pass
content = self.contents[(creator,name)]
transport.set_sid(content.transport.sid)
transport.set_file_props(content.transport.file_props)
content.transport = transport
# The content will have to be resend now that it is modified
content.sent = False
content.accepted = True
def on_session_state_changed(self, content=None): def on_session_state_changed(self, content=None):
if self.state == JingleStates.ended: if self.state == JingleStates.ended:
@ -216,10 +257,15 @@ class JingleSession(object):
elif content and self.weinitiate: elif content and self.weinitiate:
self.__content_accept(content) self.__content_accept(content)
elif self.state == JingleStates.active: elif self.state == JingleStates.active:
# We can either send a content-add or a content-accept # We can either send a content-add or a content-accept. However, if
# we are sending a file we can only use session_initiate.
if not content: if not content:
return return
if (content.creator == 'initiator') == self.weinitiate: we_created_content = (content.creator == 'initiator') \
== self.weinitiate
if we_created_content and content.media == 'file':
self.__session_initiate()
if we_created_content:
# We initiated this content. It's a pending content-add. # We initiated this content. It's a pending content-add.
self.__content_add(content) self.__content_add(content)
else: else:
@ -261,6 +307,7 @@ class JingleSession(object):
stanza, jingle = self.__make_jingle('transport-info') stanza, jingle = self.__make_jingle('transport-info')
jingle.addChild(node=content) jingle.addChild(node=content)
self.connection.connection.send(stanza) self.connection.connection.send(stanza)
self.collect_iq_id(stanza.getID())
def send_description_info(self, content): def send_description_info(self, content):
assert self.state != JingleStates.ended assert self.state != JingleStates.ended
@ -286,7 +333,7 @@ class JingleSession(object):
self.__send_error(stanza, 'bad-request') self.__send_error(stanza, 'bad-request')
return return
# FIXME: If we aren't initiated and it's not a session-initiate... # FIXME: If we aren't initiated and it's not a session-initiate...
if action != 'session-initiate' and self.state == JingleStates.ended: if action not in ['session-initiate','session-terminate'] and self.state == JingleStates.ended:
self.__send_error(stanza, 'item-not-found', 'unknown-session') self.__send_error(stanza, 'item-not-found', 'unknown-session')
return return
else: else:
@ -311,6 +358,7 @@ class JingleSession(object):
Default callback for action stanzas -- simple ack and stop processing Default callback for action stanzas -- simple ack and stop processing
""" """
response = stanza.buildReply('result') response = stanza.buildReply('result')
response.delChild(response.getQuery())
self.connection.connection.send(response) self.connection.connection.send(response)
def __on_error(self, stanza, jingle, error, action): def __on_error(self, stanza, jingle, error, action):
@ -325,18 +373,42 @@ class JingleSession(object):
error_name = child.getName() error_name = child.getName()
self.__dispatch_error(error_name, text, error.getAttr('type')) self.__dispatch_error(error_name, text, error.getAttr('type'))
# FIXME: Not sure when we would want to do that... # FIXME: Not sure when we would want to do that...
def transport_replace(self):
transport = JingleTransportIBB()
# For debug only, delete this and replace for a function
# that will identify contents by its sid
for creator, name in self.contents.keys():
self.modify_content(creator, name, transport)
cont = self.contents[(creator, name)]
cont.transport = transport
stanza, jingle = self.__make_jingle('transport-replace')
self.__append_contents(jingle)
self.__broadcast(stanza, jingle, None, 'transport-replace')
self.connection.connection.send(stanza)
self.state = JingleStates.pending
def __on_transport_replace(self, stanza, jingle, error, action): def __on_transport_replace(self, stanza, jingle, error, action):
for content in jingle.iterTags('content'): for content in jingle.iterTags('content'):
creator = content['creator'] creator = content['creator']
name = content['name'] name = content['name']
if (creator, name) in self.contents: if (creator, name) in self.contents:
transport_ns = content.getTag('transport').getNamespace() transport_ns = content.getTag('transport').getNamespace()
if transport_ns == xmpp.JINGLE_ICE_UDP: if transport_ns == xmpp.NS_JINGLE_ICE_UDP:
# FIXME: We don't manage anything else than ICE-UDP now... # FIXME: We don't manage anything else than ICE-UDP now...
# What was the previous transport?!? # What was the previous transport?!?
# Anyway, content's transport is not modifiable yet # Anyway, content's transport is not modifiable yet
pass pass
elif transport_ns == xmpp.NS_JINGLE_IBB:
transport = JingleTransportIBB()
self.modify_content(creator, name, transport)
self.state = JingleStates.pending
self.contents[(creator,name)].state = STATE_TRANSPORT_REPLACE
self.__ack(stanza, jingle, error, action)
self.__session_accept()
else: else:
stanza, jingle = self.__make_jingle('transport-reject') stanza, jingle = self.__make_jingle('transport-reject')
content = jingle.setTag('content', attrs={'creator': creator, content = jingle.setTag('content', attrs={'creator': creator,
@ -357,9 +429,22 @@ class JingleSession(object):
def __on_session_info(self, stanza, jingle, error, action): def __on_session_info(self, stanza, jingle, error, action):
# TODO: ringing, active, (un)hold, (un)mute # TODO: ringing, active, (un)hold, (un)mute
payload = jingle.getPayload() payload = jingle.getPayload()
if payload: for p in payload:
self.__send_error(stanza, 'feature-not-implemented', 'unsupported-info', type_='modify') if p.getName() == 'checksum':
raise xmpp.NodeProcessed hashes = p.getTag('file').getTag(name='hashes',
namespace=xmpp.NS_HASHES)
for hash in hashes.getChildren():
algo = hash.getAttr('algo')
if algo in xmpp.Hashes.supported:
self.hash_algo = algo
data = hash.getData()
# This only works because there is only one session
# per file in jingleFT
self.file_hash = data
raise xmpp.NodeProcessed
self.__send_error(stanza, 'feature-not-implemented', 'unsupported-info', type_='modify')
raise xmpp.NodeProcessed
def __on_content_remove(self, stanza, jingle, error, action): def __on_content_remove(self, stanza, jingle, error, action):
for content in jingle.iterTags('content'): for content in jingle.iterTags('content'):
@ -382,6 +467,7 @@ class JingleSession(object):
if self.state != JingleStates.pending: if self.state != JingleStates.pending:
raise OutOfOrder raise OutOfOrder
self.state = JingleStates.active self.state = JingleStates.active
def __on_content_accept(self, stanza, jingle, error, action): def __on_content_accept(self, stanza, jingle, error, action):
""" """
@ -429,19 +515,25 @@ class JingleSession(object):
# subscription) and the receiver has a policy of not communicating via # subscription) and the receiver has a policy of not communicating via
# Jingle with unknown entities, it SHOULD return a <service-unavailable/> # Jingle with unknown entities, it SHOULD return a <service-unavailable/>
# error. # error.
# Check if there's already a session with this user:
for session in self.connection.iter_jingle_sessions(self.peerjid):
if not session is self:
reason = xmpp.Node('reason')
alternative_session = reason.setTag('alternative-session')
alternative_session.setTagData('sid', session.sid)
self.__ack(stanza, jingle, error, action)
self._session_terminate(reason)
raise xmpp.NodeProcessed
# Lets check what kind of jingle session does the peer want # Lets check what kind of jingle session does the peer want
contents, contents_rejected, reason_txt = self.__parse_contents(jingle) contents, contents_rejected, reason_txt = self.__parse_contents(jingle)
# If we are not receivin a file
# Check if there's already a session with this user:
if contents[0][0] != 'file':
for session in self.connection.iter_jingle_sessions(self.peerjid):
if not session is self:
reason = xmpp.Node('reason')
alternative_session = reason.setTag('alternative-session')
alternative_session.setTagData('sid', session.sid)
self.__ack(stanza, jingle, error, action)
self._session_terminate(reason)
raise xmpp.NodeProcessed
# If there's no content we understand... # If there's no content we understand...
if not contents: if not contents:
@ -462,6 +554,17 @@ class JingleSession(object):
""" """
Broadcast the stanza contents to proper content handlers Broadcast the stanza contents to proper content handlers
""" """
#if jingle is None: # it is a iq-result stanza
# for cn in self.contents.values():
# cn.on_stanza(stanza, None, error, action)
# return
# special case: iq-result stanza does not come with a jingle element
if action == 'iq-result':
for cn in self.contents.values():
cn.on_stanza(stanza, None, error, action)
return
for content in jingle.iterTags('content'): for content in jingle.iterTags('content'):
name = content['name'] name = content['name']
creator = content['creator'] creator = content['creator']
@ -483,9 +586,11 @@ class JingleSession(object):
else: else:
# TODO # TODO
text = reason text = reason
gajim.nec.push_incoming_event(JingleDisconnectedReceivedEvent(None,
conn=self.connection, jingle_session=self, media=None, if reason == 'cancel' and self.session_type_FT:
reason=text)) gajim.nec.push_incoming_event(JingleTransferCancelledEvent(None,
conn=self.connection, jingle_session=self, media=None,
reason=text))
def __broadcast_all(self, stanza, jingle, error, action): def __broadcast_all(self, stanza, jingle, error, action):
""" """
@ -502,6 +607,8 @@ class JingleSession(object):
for element in jingle.iterTags('content'): for element in jingle.iterTags('content'):
transport = get_jingle_transport(element.getTag('transport')) transport = get_jingle_transport(element.getTag('transport'))
if transport:
transport.ourjid = self.ourjid
content_type = get_jingle_content(element.getTag('description')) content_type = get_jingle_content(element.getTag('description'))
if content_type: if content_type:
try: try:
@ -531,12 +638,15 @@ class JingleSession(object):
return (contents, contents_rejected, failure_reason) return (contents, contents_rejected, failure_reason)
def __dispatch_error(self, error=None, text=None, type_=None): def __dispatch_error(self, error=None, text=None, type_=None):
if text: if text:
text = '%s (%s)' % (error, text) text = '%s (%s)' % (error, text)
if type_ != 'modify': if type_ != 'modify':
gajim.nec.push_incoming_event(JingleErrorReceivedEvent(None, gajim.nec.push_incoming_event(JingleErrorReceivedEvent(None,
conn=self.connection, jingle_session=self, conn=self.connection, jingle_session=self,
reason=text or error)) reason=text or error))
def __reason_from_stanza(self, stanza): def __reason_from_stanza(self, stanza):
# TODO: Move to GUI? # TODO: Move to GUI?
@ -557,13 +667,12 @@ class JingleSession(object):
return (reason, text) return (reason, text)
def __make_jingle(self, action, reason=None): def __make_jingle(self, action, reason=None):
stanza = xmpp.Iq(typ='set', to=xmpp.JID(self.peerjid)) stanza = xmpp.Iq(typ='set', to=xmpp.JID(self.peerjid),
frm=self.ourjid)
attrs = {'action': action, attrs = {'action': action,
'sid': self.sid} 'sid': self.sid,
if action == 'session-initiate': 'initiator' : self.initiator}
attrs['initiator'] = self.initiator
elif action == 'session-accept':
attrs['responder'] = self.responder
jingle = stanza.addChild('jingle', attrs=attrs, namespace=xmpp.NS_JINGLE) jingle = stanza.addChild('jingle', attrs=attrs, namespace=xmpp.NS_JINGLE)
if reason is not None: if reason is not None:
jingle.addChild(node=reason) jingle.addChild(node=reason)
@ -605,6 +714,7 @@ class JingleSession(object):
self.__append_contents(jingle) self.__append_contents(jingle)
self.__broadcast(stanza, jingle, None, 'session-initiate-sent') self.__broadcast(stanza, jingle, None, 'session-initiate-sent')
self.connection.connection.send(stanza) self.connection.connection.send(stanza)
self.collect_iq_id(stanza.getID())
self.state = JingleStates.pending self.state = JingleStates.pending
def __session_accept(self): def __session_accept(self):
@ -613,6 +723,7 @@ class JingleSession(object):
self.__append_contents(jingle) self.__append_contents(jingle)
self.__broadcast(stanza, jingle, None, 'session-accept-sent') self.__broadcast(stanza, jingle, None, 'session-accept-sent')
self.connection.connection.send(stanza) self.connection.connection.send(stanza)
self.collect_iq_id(stanza.getID())
self.state = JingleStates.active self.state = JingleStates.active
def __session_info(self, payload=None): def __session_info(self, payload=None):
@ -621,9 +732,15 @@ class JingleSession(object):
if payload: if payload:
jingle.addChild(node=payload) jingle.addChild(node=payload)
self.connection.connection.send(stanza) self.connection.connection.send(stanza)
def _JingleFileTransfer__session_info(self, p):
# For some strange reason when I call
# self.session.__session_info(h) from the jingleFileTransfer object
# within a thread, this method gets called instead. Even though, it
# isn't being called explicitly.
self.__session_info(p)
def _session_terminate(self, reason=None): def _session_terminate(self, reason=None):
assert self.state != JingleStates.ended
stanza, jingle = self.__make_jingle('session-terminate', reason=reason) stanza, jingle = self.__make_jingle('session-terminate', reason=reason)
self.__broadcast_all(stanza, jingle, None, 'session-terminate-sent') self.__broadcast_all(stanza, jingle, None, 'session-terminate-sent')
if self.connection.connection and self.connection.connected >= 2: if self.connection.connection and self.connection.connected >= 2:
@ -647,7 +764,8 @@ class JingleSession(object):
stanza, jingle = self.__make_jingle('content-add') stanza, jingle = self.__make_jingle('content-add')
self.__append_content(jingle, content) self.__append_content(jingle, content)
self.__broadcast(stanza, jingle, None, 'content-add-sent') self.__broadcast(stanza, jingle, None, 'content-add-sent')
self.connection.connection.send(stanza) id_ = self.connection.connection.send(stanza)
self.collect_iq_id(id_)
def __content_accept(self, content): def __content_accept(self, content):
# TODO: test # TODO: test
@ -655,7 +773,8 @@ class JingleSession(object):
stanza, jingle = self.__make_jingle('content-accept') stanza, jingle = self.__make_jingle('content-accept')
self.__append_content(jingle, content) self.__append_content(jingle, content)
self.__broadcast(stanza, jingle, None, 'content-accept-sent') self.__broadcast(stanza, jingle, None, 'content-accept-sent')
self.connection.connection.send(stanza) id_ = self.connection.connection.send(stanza)
self.collect_iq_id(id_)
def __content_reject(self, content): def __content_reject(self, content):
assert self.state != JingleStates.ended assert self.state != JingleStates.ended

View file

@ -16,21 +16,29 @@ Handles Jingle Transports (currently only ICE-UDP)
""" """
import xmpp import xmpp
import socket
from common import gajim
from common.protocol.bytestream import ConnectionSocks5Bytestream
import logging
log = logging.getLogger('gajim.c.jingle_transport')
transports = {} transports = {}
def get_jingle_transport(node): def get_jingle_transport(node):
namespace = node.getNamespace() namespace = node.getNamespace()
if namespace in transports: if namespace in transports:
return transports[namespace]() return transports[namespace](node)
class TransportType(object): class TransportType(object):
""" """
Possible types of a JingleTransport Possible types of a JingleTransport
""" """
datagram = 1 ICEUDP = 1
streaming = 2 SOCKS5 = 2
IBB = 3
class JingleTransport(object): class JingleTransport(object):
@ -70,16 +78,258 @@ class JingleTransport(object):
Return the list of transport candidates from a transport stanza Return the list of transport candidates from a transport stanza
""" """
return [] return []
def set_connection(self, conn):
self.connection = conn
if not self.sid:
self.sid = self.connection.connection.getAnID()
def set_file_props(self, file_props):
self.file_props = file_props
def set_our_jid(self, jid):
self.ourjid = jid
def set_sid(self, sid):
self.sid = sid
class JingleTransportSocks5(JingleTransport):
"""
Socks5 transport in jingle scenario
Note: Don't forget to call set_file_props after initialization
"""
def __init__(self, node=None):
JingleTransport.__init__(self, TransportType.SOCKS5)
self.connection = None
self.remote_candidates = []
self.sid = None
if node and node.getAttr('sid'):
self.sid = node.getAttr('sid')
def make_candidate(self, candidate):
import logging
log = logging.getLogger()
log.info('candidate dict, %s' % candidate)
attrs = {
'cid': candidate['candidate_id'],
'host': candidate['host'],
'jid': candidate['jid'],
'port': candidate['port'],
'priority': candidate['priority'],
'type': candidate['type']
}
return xmpp.Node('candidate', attrs=attrs)
def make_transport(self, candidates=None, add_candidates = True):
if add_candidates:
self._add_local_ips_as_candidates()
self._add_additional_candidates()
self._add_proxy_candidates()
transport = JingleTransport.make_transport(self, candidates)
else:
transport = xmpp.Node('transport')
transport.setNamespace(xmpp.NS_JINGLE_BYTESTREAM)
transport.setAttr('sid', self.sid)
return transport
def parse_transport_stanza(self, transport):
candidates = []
for candidate in transport.iterTags('candidate'):
typ = 'direct' # default value
if candidate.has_attr('type'):
typ = candidate['type']
cand = {
'state': 0,
'target': self.ourjid,
'host': candidate['host'],
'port': int(candidate['port']),
'cid': candidate['cid'],
'type': typ,
'priority': candidate['priority']
}
candidates.append(cand)
# we need this when we construct file_props on session-initiation
self.remote_candidates = candidates
return candidates
def _add_candidates(self, candidates):
for cand in candidates:
in_remote = False
for cand2 in self.remote_candidates:
if cand['host'] == cand2['host'] and \
cand['port'] == cand2['port']:
in_remote = True
break
if not in_remote:
self.candidates.append(cand)
def _add_local_ips_as_candidates(self):
if not self.connection:
return
local_ip_cand = []
port = int(gajim.config.get('file_transfers_port'))
type_preference = 126 #type preference of connection type. XEP-0260 section 2.2
c = {'host': self.connection.peerhost[0]}
c['candidate_id'] = self.connection.connection.getAnID()
c['port'] = port
c['type'] = 'direct'
c['jid'] = self.ourjid
c['priority'] = (2**16) * type_preference
local_ip_cand.append(c)
for addr in socket.getaddrinfo(socket.gethostname(), None):
if not addr[4][0] in local_ip_cand and not addr[4][0].startswith('127'):
c = {'host': addr[4][0]}
c['candidate_id'] = self.connection.connection.getAnID()
c['port'] = port
c['type'] = 'direct'
c['jid'] = self.ourjid
c['priority'] = (2**16) * type_preference
c['initiator'] = self.file_props['sender']
c['target'] = self.file_props['receiver']
local_ip_cand.append(c)
self._add_candidates(local_ip_cand)
def _add_additional_candidates(self):
if not self.connection:
return
type_preference = 126
additional_ip_cand = []
port = int(gajim.config.get('file_transfers_port'))
ft_add_hosts = gajim.config.get('ft_add_hosts_to_send')
if ft_add_hosts:
hosts = [e.strip() for e in ft_add_hosts.split(',')]
for h in hosts:
c = {'host': h}
c['candidate_id'] = self.connection.connection.getAnID()
c['port'] = port
c['type'] = 'direct'
c['jid'] = self.ourjid
c['priority'] = (2**16) * type_preference
c['initiator'] = self.file_props['sender']
c['target'] = self.file_props['receiver']
additional_ip_cand.append(c)
self._add_candidates(additional_ip_cand)
def _add_proxy_candidates(self):
if not self.connection:
return
type_preference = 10
proxy_cand = []
socks5conn = self.connection
proxyhosts = socks5conn._get_file_transfer_proxies_from_config(self.file_props)
if proxyhosts:
self.file_props['proxyhosts'] = proxyhosts
for proxyhost in proxyhosts:
c = {'host': proxyhost['host']}
c['candidate_id'] = self.connection.connection.getAnID()
c['port'] = int(proxyhost['port'])
c['type'] = 'proxy'
c['jid'] = proxyhost['jid']
c['priority'] = (2**16) * type_preference
c['initiator'] = self.file_props['sender']
c['target'] = self.file_props['receiver']
proxy_cand.append(c)
self._add_candidates(proxy_cand)
def get_content(self):
sesn = self.connection.get_jingle_session(self.ourjid,
self.file_props['session-sid'])
for content in sesn.contents.values():
if content.transport == self:
return content
def _on_proxy_auth_ok(self, proxy):
log.info('proxy auth ok for ' + str(proxy))
# send activate request to proxy, send activated confirmation to peer
if not self.connection:
return
sesn = self.connection.get_jingle_session(self.ourjid,
self.file_props['session-sid'])
if sesn is None:
return
iq = xmpp.Iq(to=proxy['jid'], frm=self.ourjid, typ='set')
auth_id = "au_" + proxy['sid']
iq.setID(auth_id)
query = iq.setTag('query', namespace=xmpp.NS_BYTESTREAM)
query.setAttr('sid', proxy['sid'])
activate = query.setTag('activate')
activate.setData(sesn.peerjid)
iq.setID(auth_id)
self.connection.connection.send(iq)
content = xmpp.Node('content')
content.setAttr('creator', 'initiator')
c = self.get_content()
content.setAttr('name', c.name)
transport = xmpp.Node('transport')
transport.setNamespace(xmpp.NS_JINGLE_BYTESTREAM)
transport.setAttr('sid', proxy['sid'])
activated = xmpp.Node('activated')
cid = None
if 'cid' in proxy:
cid = proxy['cid']
else:
for host in self.candidates:
if host['host'] == proxy['host'] and host['jid'] == proxy['jid'] \
and host['port'] == proxy['port']:
cid = host['candidate_id']
break
if cid is None:
raise Exception, 'cid is missing'
activated.setAttr('cid', cid)
transport.addChild(node=activated)
content.addChild(node=transport)
sesn.send_transport_info(content)
class JingleTransportIBB(JingleTransport):
def __init__(self, node=None, block_sz=None):
JingleTransport.__init__(self, TransportType.IBB)
if block_sz:
self.block_sz = block_sz
else:
self.block_sz = '4096'
self.connection = None
self.sid = None
if node and node.getAttr('sid'):
self.sid = node.getAttr('sid')
def make_transport(self):
transport = xmpp.Node('transport')
transport.setNamespace(xmpp.NS_JINGLE_IBB)
transport.setAttr('block-size', self.block_sz)
transport.setAttr('sid', self.sid)
return transport
try: try:
import farstream import farstream
except Exception: except Exception:
pass pass
class JingleTransportICEUDP(JingleTransport): class JingleTransportICEUDP(JingleTransport):
def __init__(self): def __init__(self, node):
JingleTransport.__init__(self, TransportType.datagram) JingleTransport.__init__(self, TransportType.ICEUDP)
def make_candidate(self, candidate): def make_candidate(self, candidate):
types = {farstream.CANDIDATE_TYPE_HOST: 'host', types = {farstream.CANDIDATE_TYPE_HOST: 'host',
@ -149,3 +399,5 @@ class JingleTransportICEUDP(JingleTransport):
return candidates return candidates
transports[xmpp.NS_JINGLE_ICE_UDP] = JingleTransportICEUDP transports[xmpp.NS_JINGLE_ICE_UDP] = JingleTransportICEUDP
transports[xmpp.NS_JINGLE_BYTESTREAM] = JingleTransportSocks5
transports[xmpp.NS_JINGLE_IBB] = JingleTransportIBB

239
src/common/jingle_xtls.py Normal file
View file

@ -0,0 +1,239 @@
# -*- coding:utf-8 -*-
## src/common/jingle_xtls.py
##
## This file is part of Gajim.
##
## Gajim is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
## by the Free Software Foundation; version 3 only.
##
## Gajim is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
##
import os
import logging
import common
from common import gajim
log = logging.getLogger('gajim.c.jingle_xtls')
PYOPENSSL_PRESENT = False
pending_contents = {} # key-exchange id -> session, accept that session once key-exchange completes
def key_exchange_pend(id_, content):
pending_contents[id_] = content
def approve_pending_content(id_):
content = pending_contents[id_]
content.session.approve_session()
content.session.approve_content('file', name=content.name)
try:
import OpenSSL
PYOPENSSL_PRESENT = True
except ImportError:
log.info("PyOpenSSL not available")
if PYOPENSSL_PRESENT:
from OpenSSL import SSL
from OpenSSL.SSL import Context
from OpenSSL import crypto
TYPE_RSA = crypto.TYPE_RSA
TYPE_DSA = crypto.TYPE_DSA
SELF_SIGNED_CERTIFICATE = 'localcert'
def default_callback(connection, certificate, error_num, depth, return_code):
log.info("certificate: %s" % certificate)
return return_code
def load_cert_file(cert_path, cert_store):
"""
This is almost identical to the one in common.xmpp.tls_nb
"""
if not os.path.isfile(cert_path):
return
try:
f = open(cert_path)
except IOError, e:
log.warning('Unable to open certificate file %s: %s' % \
(cert_path, str(e)))
return
lines = f.readlines()
i = 0
begin = -1
for line in lines:
if 'BEGIN CERTIFICATE' in line:
begin = i
elif 'END CERTIFICATE' in line and begin > -1:
cert = ''.join(lines[begin:i+2])
try:
x509cert = OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_PEM, cert)
cert_store.add_cert(x509cert)
except OpenSSL.crypto.Error, exception_obj:
log.warning('Unable to load a certificate from file %s: %s' %\
(cert_path, exception_obj.args[0][0][2]))
except:
log.warning('Unknown error while loading certificate from file '
'%s' % cert_path)
begin = -1
i += 1
def get_context(fingerprint, verify_cb=None):
"""
constructs and returns the context objects
"""
ctx = SSL.Context(SSL.TLSv1_METHOD)
if fingerprint == 'server': # for testing purposes only
ctx.set_verify(SSL.VERIFY_NONE|SSL.VERIFY_FAIL_IF_NO_PEER_CERT, verify_cb or default_callback)
elif fingerprint == 'client':
ctx.set_verify(SSL.VERIFY_PEER, verify_cb or default_callback)
cert_name = os.path.join(gajim.MY_CERT_DIR, SELF_SIGNED_CERTIFICATE)
ctx.use_privatekey_file (cert_name + '.pkey')
ctx.use_certificate_file(cert_name + '.cert')
store = ctx.get_cert_store()
for f in os.listdir(os.path.expanduser(gajim.MY_PEER_CERTS_PATH)):
load_cert_file(os.path.join(os.path.expanduser(gajim.MY_PEER_CERTS_PATH), f), store)
log.debug('certificate file ' + f + ' loaded fingerprint ' + \
fingerprint)
return ctx
def send_cert(con, jid_from, sid):
certpath = os.path.join(gajim.MY_CERT_DIR, SELF_SIGNED_CERTIFICATE) + '.cert'
certfile = open(certpath, 'r')
certificate = ''
for line in certfile.readlines():
if not line.startswith('-'):
certificate += line
iq = common.xmpp.Iq('result', to=jid_from);
iq.setAttr('id', sid)
pubkey = iq.setTag('pubkeys')
pubkey.setNamespace(common.xmpp.NS_PUBKEY_PUBKEY)
keyinfo = pubkey.setTag('keyinfo')
name = keyinfo.setTag('name')
name.setData('CertificateHash')
cert = keyinfo.setTag('x509cert')
cert.setData(certificate)
con.send(iq)
def handle_new_cert(con, obj, jid_from):
jid = gajim.get_jid_without_resource(jid_from)
certpath = os.path.join(os.path.expanduser(gajim.MY_PEER_CERTS_PATH), jid)
certpath += '.cert'
id_ = obj.getAttr('id')
x509cert = obj.getTag('pubkeys').getTag('keyinfo').getTag('x509cert')
cert = x509cert.getData()
f = open(certpath, 'w')
f.write('-----BEGIN CERTIFICATE-----\n')
f.write(cert)
f.write('-----END CERTIFICATE-----\n')
approve_pending_content(id_)
def send_cert_request(con, to_jid):
iq = common.xmpp.Iq('get', to=to_jid)
id_ = con.connection.getAnID()
iq.setAttr('id', id_)
pubkey = iq.setTag('pubkeys')
pubkey.setNamespace(common.xmpp.NS_PUBKEY_PUBKEY)
con.connection.send(iq)
return unicode(id_)
# the following code is partly due to pyopenssl examples
def createKeyPair(type, bits):
"""
Create a public/private key pair.
Arguments: type - Key type, must be one of TYPE_RSA and TYPE_DSA
bits - Number of bits to use in the key
Returns: The public/private key pair in a PKey object
"""
pkey = crypto.PKey()
pkey.generate_key(type, bits)
return pkey
def createCertRequest(pkey, digest="md5", **name):
"""
Create a certificate request.
Arguments: pkey - The key to associate with the request
digest - Digestion method to use for signing, default is md5
**name - The name of the subject of the request, possible
arguments are:
C - Country name
ST - State or province name
L - Locality name
O - Organization name
OU - Organizational unit name
CN - Common name
emailAddress - E-mail address
Returns: The certificate request in an X509Req object
"""
req = crypto.X509Req()
subj = req.get_subject()
for (key,value) in name.items():
setattr(subj, key, value)
req.set_pubkey(pkey)
req.sign(pkey, digest)
return req
def createCertificate(req, (issuerCert, issuerKey), serial, (notBefore, notAfter), digest="md5"):
"""
Generate a certificate given a certificate request.
Arguments: req - Certificate reqeust to use
issuerCert - The certificate of the issuer
issuerKey - The private key of the issuer
serial - Serial number for the certificate
notBefore - Timestamp (relative to now) when the certificate
starts being valid
notAfter - Timestamp (relative to now) when the certificate
stops being valid
digest - Digest method to use for signing, default is md5
Returns: The signed certificate in an X509 object
"""
cert = crypto.X509()
cert.set_serial_number(serial)
cert.gmtime_adj_notBefore(notBefore)
cert.gmtime_adj_notAfter(notAfter)
cert.set_issuer(issuerCert.get_subject())
cert.set_subject(req.get_subject())
cert.set_pubkey(req.get_pubkey())
cert.sign(issuerKey, digest)
return cert
def make_certs(filepath, CN):
"""
make self signed certificates
filepath : absolute path of certificate file, will be appended the '.pkey' and '.cert' extensions
CN : common name
"""
key = createKeyPair(TYPE_RSA, 1024)
req = createCertRequest(key, CN=CN)
cert = createCertificate(req, (req, key), 0, (0, 60*60*24*365*5)) # five years
open(filepath + '.pkey', 'w').write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
open(filepath + '.cert', 'w').write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
if __name__ == '__main__':
make_certs('./selfcert', 'gajim')

View file

@ -37,9 +37,8 @@ from common import xmpp
from common import gajim from common import gajim
from common import helpers from common import helpers
from common import dataforms from common import dataforms
from common.connection_handlers_events import FileRequestReceivedEvent, \
FileRequestErrorEvent, InformationEvent
from common import ged from common import ged
from common import jingle_xtls
from common.socks5 import Socks5Receiver from common.socks5 import Socks5Receiver
@ -140,6 +139,34 @@ class ConnectionBytestream:
# user response to ConfirmationDialog may come after we've disconneted # user response to ConfirmationDialog may come after we've disconneted
if not self.connection or self.connected < 2: if not self.connection or self.connected < 2:
return return
# file transfer initiated by a jingle session
log.info("send_file_approval: jingle session accept")
if file_props.get('session-type') == 'jingle':
session = self.get_jingle_session(file_props['sender'],
file_props['session-sid'])
if not session:
return
content = None
for c in session.contents.values():
if c.transport.sid == file_props['sid']:
content = c
break
if not content:
return
gajim.socks5queue.add_file_props(self.name, file_props)
if not session.accepted:
if session.get_content('file', content.name).use_security:
id_ = jingle_xtls.send_cert_request(self,
file_props['sender'])
jingle_xtls.key_exchange_pend(id_, content)
return
session.approve_session()
session.approve_content('file', content.name)
return
iq = xmpp.Iq(to=unicode(file_props['sender']), typ='result') iq = xmpp.Iq(to=unicode(file_props['sender']), typ='result')
iq.setAttr('id', file_props['request-id']) iq.setAttr('id', file_props['request-id'])
si = iq.setTag('si', namespace=xmpp.NS_SI) si = iq.setTag('si', namespace=xmpp.NS_SI)
@ -168,6 +195,10 @@ class ConnectionBytestream:
# user response to ConfirmationDialog may come after we've disconneted # user response to ConfirmationDialog may come after we've disconneted
if not self.connection or self.connected < 2: if not self.connection or self.connected < 2:
return return
if file_props['session-type'] == 'jingle':
jingle = self._sessions[file_props['session-sid']]
jingle.cancel_session()
return
iq = xmpp.Iq(to=unicode(file_props['sender']), typ='error') iq = xmpp.Iq(to=unicode(file_props['sender']), typ='error')
iq.setAttr('id', file_props['request-id']) iq.setAttr('id', file_props['request-id'])
if code == '400' and typ in ('stream', 'profile'): if code == '400' and typ in ('stream', 'profile'):
@ -221,6 +252,7 @@ class ConnectionBytestream:
raise xmpp.NodeProcessed raise xmpp.NodeProcessed
def _siSetCB(self, con, iq_obj): def _siSetCB(self, con, iq_obj):
from common.connection_handlers_events import FileRequestReceivedEvent
gajim.nec.push_incoming_event(FileRequestReceivedEvent(None, conn=self, gajim.nec.push_incoming_event(FileRequestReceivedEvent(None, conn=self,
stanza=iq_obj)) stanza=iq_obj))
raise xmpp.NodeProcessed raise xmpp.NodeProcessed
@ -240,6 +272,7 @@ class ConnectionBytestream:
return return
jid = self._ft_get_from(iq_obj) jid = self._ft_get_from(iq_obj)
file_props['error'] = -3 file_props['error'] = -3
from common.connection_handlers_events import FileRequestErrorEvent
gajim.nec.push_incoming_event(FileRequestErrorEvent(None, conn=self, gajim.nec.push_incoming_event(FileRequestErrorEvent(None, conn=self,
jid=jid, file_props=file_props, error_msg='')) jid=jid, file_props=file_props, error_msg=''))
raise xmpp.NodeProcessed raise xmpp.NodeProcessed
@ -273,6 +306,8 @@ class ConnectionSocks5Bytestream(ConnectionBytestream):
if contact.get_full_jid() == receiver_jid: if contact.get_full_jid() == receiver_jid:
file_props['error'] = -5 file_props['error'] = -5
self.remove_transfer(file_props) self.remove_transfer(file_props)
from common.connection_handlers_events import \
FileRequestErrorEvent
gajim.nec.push_incoming_event(FileRequestErrorEvent(None, gajim.nec.push_incoming_event(FileRequestErrorEvent(None,
conn=self, jid=contact.jid, file_props=file_props, conn=self, jid=contact.jid, file_props=file_props,
error_msg='')) error_msg=''))
@ -332,9 +367,10 @@ class ConnectionSocks5Bytestream(ConnectionBytestream):
port = gajim.config.get('file_transfers_port') port = gajim.config.get('file_transfers_port')
listener = gajim.socks5queue.start_listener(port, sha_str, listener = gajim.socks5queue.start_listener(port, sha_str,
self._result_socks5_sid, file_props['sid']) self._result_socks5_sid, file_props)
if not listener: if not listener:
file_props['error'] = -5 file_props['error'] = -5
from common.connection_handlers_events import FileRequestErrorEvent
gajim.nec.push_incoming_event(FileRequestErrorEvent(None, conn=self, gajim.nec.push_incoming_event(FileRequestErrorEvent(None, conn=self,
jid=unicode(receiver), file_props=file_props, error_msg='')) jid=unicode(receiver), file_props=file_props, error_msg=''))
self._connect_error(unicode(receiver), file_props['sid'], self._connect_error(unicode(receiver), file_props['sid'],
@ -374,6 +410,7 @@ class ConnectionSocks5Bytestream(ConnectionBytestream):
port = gajim.config.get('file_transfers_port') port = gajim.config.get('file_transfers_port')
self._add_streamhosts_to_query(query, sender, port, my_ips) self._add_streamhosts_to_query(query, sender, port, my_ips)
except socket.gaierror: except socket.gaierror:
from common.connection_handlers_events import InformationEvent
gajim.nec.push_incoming_event(InformationEvent(None, conn=self, gajim.nec.push_incoming_event(InformationEvent(None, conn=self,
level='error', pri_txt=_('Wrong host'), level='error', pri_txt=_('Wrong host'),
sec_txt=_('Invalid local address? :-O'))) sec_txt=_('Invalid local address? :-O')))
@ -546,6 +583,8 @@ class ConnectionSocks5Bytestream(ConnectionBytestream):
if file_props is not None: if file_props is not None:
self.disconnect_transfer(file_props) self.disconnect_transfer(file_props)
file_props['error'] = -3 file_props['error'] = -3
from common.connection_handlers_events import \
FileRequestErrorEvent
gajim.nec.push_incoming_event(FileRequestErrorEvent(None, gajim.nec.push_incoming_event(FileRequestErrorEvent(None,
conn=self, jid=to, file_props=file_props, error_msg=msg)) conn=self, jid=to, file_props=file_props, error_msg=msg))
@ -578,6 +617,7 @@ class ConnectionSocks5Bytestream(ConnectionBytestream):
return return
file_props = self.files_props[id_] file_props = self.files_props[id_]
file_props['error'] = -4 file_props['error'] = -4
from common.connection_handlers_events import FileRequestErrorEvent
gajim.nec.push_incoming_event(FileRequestErrorEvent(None, conn=self, gajim.nec.push_incoming_event(FileRequestErrorEvent(None, conn=self,
jid=jid, file_props=file_props, error_msg='')) jid=jid, file_props=file_props, error_msg=''))
raise xmpp.NodeProcessed raise xmpp.NodeProcessed
@ -713,7 +753,10 @@ class ConnectionSocks5Bytestream(ConnectionBytestream):
raise xmpp.NodeProcessed raise xmpp.NodeProcessed
else: else:
gajim.socks5queue.send_file(file_props, self.name) if 'stopped' in file_props and file_props['stopped']:
self.remove_transfer(file_props)
else:
gajim.socks5queue.send_file(file_props, self.name, 'client')
if 'fast' in file_props: if 'fast' in file_props:
fasts = file_props['fast'] fasts = file_props['fast']
if len(fasts) > 0: if len(fasts) > 0:
@ -741,9 +784,9 @@ class ConnectionIBBytestream(ConnectionBytestream):
elif typ == 'set' and stanza.getTag('close', namespace=xmpp.NS_IBB): elif typ == 'set' and stanza.getTag('close', namespace=xmpp.NS_IBB):
self.StreamCloseHandler(conn, stanza) self.StreamCloseHandler(conn, stanza)
elif typ == 'result': elif typ == 'result':
self.StreamCommitHandler(conn, stanza) self.SendHandler()
elif typ == 'error': elif typ == 'error':
self.StreamOpenReplyHandler(conn, stanza) gajim.socks5queue.error_cb()
else: else:
conn.send(xmpp.Error(stanza, xmpp.ERR_BAD_REQUEST)) conn.send(xmpp.Error(stanza, xmpp.ERR_BAD_REQUEST))
raise xmpp.NodeProcessed raise xmpp.NodeProcessed
@ -918,13 +961,17 @@ class ConnectionIBBytestream(ConnectionBytestream):
log.debug('StreamCloseHandler called sid->%s' % sid) log.debug('StreamCloseHandler called sid->%s' % sid)
# look in sending files # look in sending files
if sid in self.files_props.keys(): if sid in self.files_props.keys():
conn.send(stanza.buildReply('result')) reply = stanza.buildReply('result')
gajim.socks5queue.complete_transfer_cb(self.name, file_props) reply.delChild('close')
conn.send(reply)
gajim.socks5queue.complete_transfer_cb(self.name, self.files_props[sid])
del self.files_props[sid] del self.files_props[sid]
# look in receiving files # look in receiving files
elif gajim.socks5queue.get_file_props(self.name, sid): elif gajim.socks5queue.get_file_props(self.name, sid):
file_props = gajim.socks5queue.get_file_props(self.name, sid) file_props = gajim.socks5queue.get_file_props(self.name, sid)
conn.send(stanza.buildReply('result')) reply = stanza.buildReply('result')
reply.delChild('close')
conn.send(reply)
file_props['fp'].close() file_props['fp'].close()
gajim.socks5queue.complete_transfer_cb(self.name, file_props) gajim.socks5queue.complete_transfer_cb(self.name, file_props)
gajim.socks5queue.remove_file_props(self.name, sid) gajim.socks5queue.remove_file_props(self.name, sid)
@ -964,6 +1011,7 @@ class ConnectionIBBytestream(ConnectionBytestream):
if stanza.getTag('data'): if stanza.getTag('data'):
if self.IBBMessageHandler(conn, stanza): if self.IBBMessageHandler(conn, stanza):
reply = stanza.buildReply('result') reply = stanza.buildReply('result')
reply.delChild('data')
conn.send(reply) conn.send(reply)
raise xmpp.NodeProcessed raise xmpp.NodeProcessed
elif syn_id == self.last_sent_ibb_id: elif syn_id == self.last_sent_ibb_id:

File diff suppressed because it is too large Load diff

View file

@ -23,6 +23,7 @@ sub- stanzas) handling routines
from simplexml import Node, NodeBuilder from simplexml import Node, NodeBuilder
import time import time
import string import string
import hashlib
def ascii_upper(s): def ascii_upper(s):
trans_table = string.maketrans(string.ascii_lowercase, trans_table = string.maketrans(string.ascii_lowercase,
@ -90,8 +91,12 @@ NS_JINGLE_ERRORS = 'urn:xmpp:jingle:errors:1' # XEP-0166
NS_JINGLE_RTP = 'urn:xmpp:jingle:apps:rtp:1' # XEP-0167 NS_JINGLE_RTP = 'urn:xmpp:jingle:apps:rtp:1' # XEP-0167
NS_JINGLE_RTP_AUDIO = 'urn:xmpp:jingle:apps:rtp:audio' # XEP-0167 NS_JINGLE_RTP_AUDIO = 'urn:xmpp:jingle:apps:rtp:audio' # XEP-0167
NS_JINGLE_RTP_VIDEO = 'urn:xmpp:jingle:apps:rtp:video' # XEP-0167 NS_JINGLE_RTP_VIDEO = 'urn:xmpp:jingle:apps:rtp:video' # XEP-0167
NS_JINGLE_FILE_TRANSFER ='urn:xmpp:jingle:apps:file-transfer:3' # XEP-0234
NS_JINGLE_XTLS='urn:xmpp:jingle:security:xtls:0' # XTLS: EXPERIMENTAL security layer of jingle
NS_JINGLE_RAW_UDP = 'urn:xmpp:jingle:transports:raw-udp:1' # XEP-0177 NS_JINGLE_RAW_UDP = 'urn:xmpp:jingle:transports:raw-udp:1' # XEP-0177
NS_JINGLE_ICE_UDP = 'urn:xmpp:jingle:transports:ice-udp:1' # XEP-0176 NS_JINGLE_ICE_UDP = 'urn:xmpp:jingle:transports:ice-udp:1' # XEP-0176
NS_JINGLE_BYTESTREAM ='urn:xmpp:jingle:transports:s5b:1' # XEP-0260
NS_JINGLE_IBB = 'urn:xmpp:jingle:transports:ibb:1' # XEP-0261
NS_LAST = 'jabber:iq:last' NS_LAST = 'jabber:iq:last'
NS_LOCATION = 'http://jabber.org/protocol/geoloc' # XEP-0080 NS_LOCATION = 'http://jabber.org/protocol/geoloc' # XEP-0080
NS_MESSAGE = 'message' # Jabberd2 NS_MESSAGE = 'message' # Jabberd2
@ -153,8 +158,16 @@ NS_DATA_LAYOUT = 'http://jabber.org/protocol/xdata-layout' # XEP-0141
NS_DATA_VALIDATE = 'http://jabber.org/protocol/xdata-validate' # XEP-0122 NS_DATA_VALIDATE = 'http://jabber.org/protocol/xdata-validate' # XEP-0122
NS_XMPP_STREAMS = 'urn:ietf:params:xml:ns:xmpp-streams' NS_XMPP_STREAMS = 'urn:ietf:params:xml:ns:xmpp-streams'
NS_RECEIPTS = 'urn:xmpp:receipts' NS_RECEIPTS = 'urn:xmpp:receipts'
NS_PUBKEY_PUBKEY = 'urn:xmpp:pubkey:2' # XEP-0189
NS_PUBKEY_REVOKE = 'urn:xmpp:revoke:2'
NS_PUBKEY_ATTEST = 'urn:xmpp:attest:2'
NS_STREAM_MGMT = 'urn:xmpp:sm:2' # XEP-198 NS_STREAM_MGMT = 'urn:xmpp:sm:2' # XEP-198
NS_HASHES = 'urn:xmpp:hashes:0' # XEP-300
NS_HASHES_MD5 = 'urn:xmpp:hash-function-textual-names:md5'
NS_HASHES_SHA1 = 'urn:xmpp:hash-function-textual-names:sha-1'
NS_HASHES_SHA256 = 'urn:xmpp:hash-function-textual-names:sha-256'
NS_HASHES_SHA512 = 'urn:xmpp:hash-function-textual-names:sha-512'
xmpp_stream_error_conditions = ''' xmpp_stream_error_conditions = '''
bad-format -- -- -- The entity has sent XML that cannot be processed. bad-format -- -- -- The entity has sent XML that cannot be processed.
bad-namespace-prefix -- -- -- The entity has sent a namespace prefix that is unsupported, or has sent no namespace prefix on an element that requires such a prefix. bad-namespace-prefix -- -- -- The entity has sent a namespace prefix that is unsupported, or has sent no namespace prefix on an element that requires such a prefix.
@ -1024,13 +1037,84 @@ class Iq(Protocol):
attrs={'id': self.getID()}) attrs={'id': self.getID()})
iq.setQuery(self.getQuery().getName()).setNamespace(self.getQueryNS()) iq.setQuery(self.getQuery().getName()).setNamespace(self.getQueryNS())
return iq return iq
class Hashes(Node):
"""
Hash elements for various XEPs as defined in XEP-300
"""
"""
RECOMENDED HASH USE:
Algorithm Support
MD2 MUST NOT
MD4 MUST NOT
MD5 MAY
SHA-1 MUST
SHA-256 MUST
SHA-512 SHOULD
"""
supported = ('md5', 'sha-1', 'sha-256', 'sha-512')
def __init__(self, nsp=NS_HASHES):
Node.__init__(self, None, {}, [], None, None,False, None)
self.setNamespace(nsp)
self.setName('hashes')
def calculateHash(self, algo, file_string):
"""
Calculate the hash and add it. It is preferable doing it here
instead of doing it all over the place in Gajim.
"""
hl = None
hash_ = None
# file_string can be a string or a file
if type(file_string) == str: # if it is a string
if algo == 'md5':
hl = hashlib.md5()
elif algo == 'sha-1':
hl = hashlib.sha1()
elif algo == 'sha-256':
hl = hashlib.sha256()
elif algo == 'sha-512':
hl = hashlib.sha512()
if hl:
hl.update(file_string)
hash_ = hl.hexdigest()
else: # if it is a file
if algo == 'md5':
hl = hashlib.md5()
elif algo == 'sha-1':
hl = hashlib.sha1()
elif algo == 'sha-256':
hl = hashlib.sha256()
elif algo == 'sha-512':
hl = hashlib.sha512()
if hl:
for line in file_string:
hl.update(line)
hash_ = hl.hexdigest()
return hash_
def addHash(self, hash_, algo):
"""
More than one hash can be added. Although it is permitted, it should
not be done for big files because it could slow down Gajim.
"""
attrs = {}
attrs['algo'] = algo
self.addChild('hash', attrs, [hash_])
class Acks(Node): class Acks(Node):
""" """
Acknowledgement elements for Stream Management Acknowledgement elements for Stream Management
""" """
def __init__(self, nsp=NS_STREAM_MGMT): def __init__(self, nsp=NS_STREAM_MGMT):
Node.__init__(self, None, {}, [], None, None,False, None) Node.__init__(self, None, {}, [], None, None, False, None)
self.setNamespace(nsp) self.setNamespace(nsp)
def buildAnswer(self, handled): def buildAnswer(self, handled):
@ -1407,3 +1491,4 @@ class DataForm(Node):
Simple dictionary interface for setting datafields values by their names Simple dictionary interface for setting datafields values by their names
""" """
return self.setField(name).setValue(val) return self.setField(name).setValue(val)

View file

@ -1615,16 +1615,15 @@ class YesNoDialog(HigDialog):
""" """
def __init__(self, pritext, sectext='', checktext='', on_response_yes=None, def __init__(self, pritext, sectext='', checktext='', on_response_yes=None,
on_response_no=None): on_response_no=None, type_=gtk.MESSAGE_QUESTION):
self.user_response_yes = on_response_yes self.user_response_yes = on_response_yes
self.user_response_no = on_response_no self.user_response_no = on_response_no
if hasattr(gajim.interface, 'roster') and gajim.interface.roster: if hasattr(gajim.interface, 'roster') and gajim.interface.roster:
parent = gajim.interface.roster.window parent = gajim.interface.roster.window
else: else:
parent = None parent = None
HigDialog.__init__(self, parent, gtk.MESSAGE_QUESTION, HigDialog.__init__(self, parent, type_, gtk.BUTTONS_YES_NO, pritext,
gtk.BUTTONS_YES_NO, pritext, sectext, sectext, on_response_yes=self.on_response_yes,
on_response_yes=self.on_response_yes,
on_response_no=self.on_response_no) on_response_no=self.on_response_no)
if checktext: if checktext:

View file

@ -35,6 +35,10 @@ from common import gajim
from common import helpers from common import helpers
from common.protocol.bytestream import (is_transfer_active, is_transfer_paused, from common.protocol.bytestream import (is_transfer_active, is_transfer_paused,
is_transfer_stopped) is_transfer_stopped)
from common.xmpp.protocol import NS_JINGLE_FILE_TRANSFER
import logging
log = logging.getLogger('gajim.filetransfer_window')
C_IMAGE = 0 C_IMAGE = 0
C_LABELS = 1 C_LABELS = 1
@ -42,7 +46,8 @@ C_FILE = 2
C_TIME = 3 C_TIME = 3
C_PROGRESS = 4 C_PROGRESS = 4
C_PERCENT = 5 C_PERCENT = 5
C_SID = 6 C_PULSE = 6
C_SID = 7
class FileTransfersWindow: class FileTransfersWindow:
@ -61,7 +66,8 @@ class FileTransfersWindow:
shall_notify = gajim.config.get('notify_on_file_complete') shall_notify = gajim.config.get('notify_on_file_complete')
self.notify_ft_checkbox.set_active(shall_notify self.notify_ft_checkbox.set_active(shall_notify
) )
self.model = gtk.ListStore(gtk.gdk.Pixbuf, str, str, str, str, int, str) self.model = gtk.ListStore(gtk.gdk.Pixbuf, str, str, str, str, int,
int, str)
self.tree.set_model(self.model) self.tree.set_model(self.model)
col = gtk.TreeViewColumn() col = gtk.TreeViewColumn()
@ -108,6 +114,7 @@ class FileTransfersWindow:
col.pack_start(renderer, expand=False) col.pack_start(renderer, expand=False)
col.add_attribute(renderer, 'text', C_PROGRESS) col.add_attribute(renderer, 'text', C_PROGRESS)
col.add_attribute(renderer, 'value', C_PERCENT) col.add_attribute(renderer, 'value', C_PERCENT)
col.add_attribute(renderer, 'pulse', C_PULSE)
col.set_resizable(True) col.set_resizable(True)
col.set_expand(False) col.set_expand(False)
self.tree.append_column(col) self.tree.append_column(col)
@ -121,6 +128,8 @@ class FileTransfersWindow:
'pause': gtk.STOCK_MEDIA_PAUSE, 'pause': gtk.STOCK_MEDIA_PAUSE,
'continue': gtk.STOCK_MEDIA_PLAY, 'continue': gtk.STOCK_MEDIA_PLAY,
'ok': gtk.STOCK_APPLY, 'ok': gtk.STOCK_APPLY,
'computing': gtk.STOCK_EXECUTE,
'hash_error': gtk.STOCK_STOP,
} }
self.tree.get_selection().set_mode(gtk.SELECTION_SINGLE) self.tree.get_selection().set_mode(gtk.SELECTION_SINGLE)
@ -240,6 +249,21 @@ class FileTransfersWindow:
dialogs.ErrorDialog(_('File transfer stopped'), sectext) dialogs.ErrorDialog(_('File transfer stopped'), sectext)
self.tree.get_selection().unselect_all() self.tree.get_selection().unselect_all()
def show_hash_error(self, jid, file_props):
def on_yes(dummy):
# TODO: Request the file to the sender
pass
if file_props['type'] == 'r':
file_name = os.path.basename(file_props['file-name'])
else:
file_name = file_props['name']
dialogs.YesNoDialog(('File transfer error'),
_('The file %(file)s has been fully received, but it seems to be '
'wrongly received.\nDo you want to reload it?') % \
{'file': file_name}, on_response_yes=on_yes,
type_=gtk.MESSAGE_ERROR)
def show_file_send_request(self, account, contact): def show_file_send_request(self, account, contact):
win = gtk.ScrolledWindow() win = gtk.ScrolledWindow()
win.set_shadow_type(gtk.SHADOW_IN) win.set_shadow_type(gtk.SHADOW_IN)
@ -310,8 +334,17 @@ class FileTransfersWindow:
file_path, file_name, file_desc) file_path, file_name, file_desc)
if file_props is None: if file_props is None:
return False return False
self.add_transfer(account, contact, file_props) if contact.supports(NS_JINGLE_FILE_TRANSFER):
gajim.connections[account].send_file_request(file_props) log.info("contact %s supports jingle file transfer"%(contact.get_full_jid()))
# this call has the side effect of setting file_props['sid'] to the jingle sid, but for the sake of clarity
# make it explicit here
sid = gajim.connections[account].start_file_transfer(contact.get_full_jid(), file_props)
file_props['sid'] = sid
self.add_transfer(account, contact, file_props)
else:
log.info("contact does not support jingle file transfer")
gajim.connections[account].send_file_request(file_props)
self.add_transfer(account, contact, file_props)
return True return True
def _start_receive(self, file_path, account, contact, file_props): def _start_receive(self, file_path, account, contact, file_props):
@ -436,6 +469,36 @@ class FileTransfersWindow:
file_props['stopped'] = True file_props['stopped'] = True
elif status == 'ok': elif status == 'ok':
file_props['completed'] = True file_props['completed'] = True
text = self._format_percent(100)
received_size = int(file_props['received-len'])
full_size = int(file_props['size'])
text += helpers.convert_bytes(received_size) + '/' + \
helpers.convert_bytes(full_size)
self.model.set(iter_, C_PROGRESS, text)
self.model.set(iter_, C_PULSE, gobject.constants.G_MAXINT)
elif status == 'computing':
self.model.set(iter_, C_PULSE, 1)
text = _('Checking file...') + '\n'
received_size = int(file_props['received-len'])
full_size = int(file_props['size'])
text += helpers.convert_bytes(received_size) + '/' + \
helpers.convert_bytes(full_size)
self.model.set(iter_, C_PROGRESS, text)
def pulse():
p = self.model.get(iter_, C_PULSE)[0]
if p == gobject.constants.G_MAXINT:
return False
self.model.set(iter_, C_PULSE, p + 1)
return True
gobject.timeout_add(100, pulse)
elif status == 'hash_error':
text = _('File error') + '\n'
received_size = int(file_props['received-len'])
full_size = int(file_props['size'])
text += helpers.convert_bytes(received_size) + '/' + \
helpers.convert_bytes(full_size)
self.model.set(iter_, C_PROGRESS, text)
self.model.set(iter_, C_PULSE, gobject.constants.G_MAXINT)
self.model.set(iter_, C_IMAGE, self.get_icon(status)) self.model.set(iter_, C_IMAGE, self.get_icon(status))
path = self.model.get_path(iter_) path = self.model.get_path(iter_)
self.select_func(path) self.select_func(path)
@ -576,7 +639,12 @@ class FileTransfersWindow:
status = 'stop' status = 'stop'
self.model.set(iter_, 0, self.get_icon(status)) self.model.set(iter_, 0, self.get_icon(status))
if transfered_size == full_size: if transfered_size == full_size:
self.set_status(typ, sid, 'ok') # If we are receiver and this is a jingle session
if file_props['type'] == 'r' and 'session-sid' in file_props:
# Show that we are computing the hash
self.set_status(typ, sid, 'computing')
else:
self.set_status(typ, sid, 'ok')
elif just_began: elif just_began:
path = self.model.get_path(iter_) path = self.model.get_path(iter_)
self.select_func(path) self.select_func(path)
@ -642,7 +710,7 @@ class FileTransfersWindow:
file_name = file_props['name'] file_name = file_props['name']
text_props = gobject.markup_escape_text(file_name) + '\n' text_props = gobject.markup_escape_text(file_name) + '\n'
text_props += contact.get_shown_name() text_props += contact.get_shown_name()
self.model.set(iter_, 1, text_labels, 2, text_props, C_SID, self.model.set(iter_, 1, text_labels, 2, text_props, C_PULSE, -1, C_SID,
file_props['type'] + file_props['sid']) file_props['type'] + file_props['sid'])
self.set_progress(file_props['type'], file_props['sid'], 0, iter_) self.set_progress(file_props['type'], file_props['sid'], 0, iter_)
if 'started' in file_props and file_props['started'] is False: if 'started' in file_props and file_props['started'] is False:

View file

@ -34,7 +34,6 @@
## You should have received a copy of the GNU General Public License ## You should have received a copy of the GNU General Public License
## along with Gajim. If not, see <http://www.gnu.org/licenses/>. ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
## ##
import os import os
import sys import sys
import re import re
@ -71,6 +70,7 @@ from session import ChatControlSession
import common.sleepy import common.sleepy
from common.xmpp import idlequeue from common.xmpp import idlequeue
from common.xmpp import Hashes
from common.zeroconf import connection_zeroconf from common.zeroconf import connection_zeroconf
from common import resolver from common import resolver
from common import caps_cache from common import caps_cache
@ -83,6 +83,7 @@ from common import logging_helpers
from common.connection_handlers_events import OurShowEvent, \ from common.connection_handlers_events import OurShowEvent, \
FileRequestErrorEvent, InformationEvent FileRequestErrorEvent, InformationEvent
from common.connection import Connection from common.connection import Connection
from common import jingle
import roster_window import roster_window
import profile_window import profile_window
@ -918,21 +919,65 @@ class Interface:
self.instances['file_transfers'].set_progress(file_props['type'], self.instances['file_transfers'].set_progress(file_props['type'],
file_props['sid'], file_props['received-len']) file_props['sid'], file_props['received-len'])
def __compare_hashes(self, account, file_props):
session = gajim.connections[account].get_jingle_session(jid=None,
sid=file_props['session-sid'])
ft_win = self.instances['file_transfers']
if not session.file_hash:
# We disn't get the hash, sender probably don't support that
jid = unicode(file_props['sender'])
self.popup_ft_result(account, jid, file_props)
ft_win.set_status(file_props['type'], file_props['sid'], 'ok')
h = Hashes()
try:
file_ = open(file_props['file-name'], 'r')
except:
return
hash_ = h.calculateHash(session.hash_algo, file_)
file_.close()
# If the hash we received and the hash of the file are the same,
# then the file is not corrupt
jid = unicode(file_props['sender'])
if session.file_hash == hash_:
self.popup_ft_result(account, jid, file_props)
ft_win.set_status(file_props['type'], file_props['sid'], 'ok')
else:
# wrong hash, we need to get the file again!
file_props['error'] = -10
self.popup_ft_result(account, jid, file_props)
ft_win.set_status(file_props['type'], file_props['sid'],
'hash_error')
# End jingle session
if session:
session.end_session()
def handle_event_file_rcv_completed(self, account, file_props): def handle_event_file_rcv_completed(self, account, file_props):
ft = self.instances['file_transfers'] ft = self.instances['file_transfers']
if file_props['error'] == 0: if file_props['error'] == 0:
ft.set_progress(file_props['type'], file_props['sid'], ft.set_progress(file_props['type'], file_props['sid'],
file_props['received-len']) file_props['received-len'])
else: else:
ft.set_status(file_props['type'], file_props['sid'], 'stop') ft.set_status(file_props['type'], file_props['sid'], 'stop')
if 'stalled' in file_props and file_props['stalled'] or \ if 'stalled' in file_props and file_props['stalled'] or \
'paused' in file_props and file_props['paused']: 'paused' in file_props and file_props['paused']:
return return
if file_props['type'] == 'r': # we receive a file if file_props['type'] == 'r': # we receive a file
jid = unicode(file_props['sender']) # If we have a jingle session id, it is a jingle transfer
# we compare hashes
if 'session-sid' in file_props:
# Compare hashes in a new thread
self.hashThread = Thread(target=self.__compare_hashes,
args=(account, file_props))
self.hashThread.start()
gajim.socks5queue.remove_receiver(file_props['sid'], True, True)
else: # we send a file else: # we send a file
jid = unicode(file_props['receiver']) jid = unicode(file_props['receiver'])
gajim.socks5queue.remove_sender(file_props['sid'], True, True)
self.popup_ft_result(account, jid, file_props)
def popup_ft_result(self, account, jid, file_props):
ft = self.instances['file_transfers']
if helpers.allow_popup_window(account): if helpers.allow_popup_window(account):
if file_props['error'] == 0: if file_props['error'] == 0:
if gajim.config.get('notify_on_file_complete'): if gajim.config.get('notify_on_file_complete'):
@ -943,6 +988,8 @@ class Interface:
elif file_props['error'] == -6: elif file_props['error'] == -6:
ft.show_stopped(jid, file_props, ft.show_stopped(jid, file_props,
error_msg=_('Error opening file')) error_msg=_('Error opening file'))
elif file_props['error'] == -10:
ft.show_hash_error(jid, file_props)
return return
msg_type = '' msg_type = ''
@ -954,6 +1001,9 @@ class Interface:
elif file_props['error'] in (-1, -6): elif file_props['error'] in (-1, -6):
msg_type = 'file-stopped' msg_type = 'file-stopped'
event_type = _('File Transfer Stopped') event_type = _('File Transfer Stopped')
elif file_props['error'] == -10:
msg_type = 'file-hash-error'
event_type = _('File Transfer Failed')
if event_type == '': if event_type == '':
# FIXME: ugly workaround (this can happen Gajim sent, Gaim recvs) # FIXME: ugly workaround (this can happen Gajim sent, Gaim recvs)
@ -971,16 +1021,20 @@ class Interface:
# get the name of the sender, as it is in the roster # get the name of the sender, as it is in the roster
sender = unicode(file_props['sender']).split('/')[0] sender = unicode(file_props['sender']).split('/')[0]
name = gajim.contacts.get_first_contact_from_jid(account, name = gajim.contacts.get_first_contact_from_jid(account,
sender).get_shown_name() sender).get_shown_name()
filename = os.path.basename(file_props['file-name']) filename = os.path.basename(file_props['file-name'])
if event_type == _('File Transfer Completed'): if event_type == _('File Transfer Completed'):
txt = _('You successfully received %(filename)s from ' txt = _('You successfully received %(filename)s from '
'%(name)s.') % {'filename': filename, 'name': name} '%(name)s.') % {'filename': filename, 'name': name}
img_name = 'gajim-ft_done' img_name = 'gajim-ft_done'
else: # ft stopped elif event_type == _('File Transfer Stopped'):
txt = _('File transfer of %(filename)s from %(name)s ' txt = _('File transfer of %(filename)s from %(name)s '
'stopped.') % {'filename': filename, 'name': name} 'stopped.') % {'filename': filename, 'name': name}
img_name = 'gajim-ft_stopped' img_name = 'gajim-ft_stopped'
else: # ft hash error
txt = _('File transfer of %(filename)s from %(name)s '
'failed.') % {'filename': filename, 'name': name}
img_name = 'gajim-ft_stopped'
else: else:
receiver = file_props['receiver'] receiver = file_props['receiver']
if hasattr(receiver, 'jid'): if hasattr(receiver, 'jid'):
@ -988,24 +1042,28 @@ class Interface:
receiver = receiver.split('/')[0] receiver = receiver.split('/')[0]
# get the name of the contact, as it is in the roster # get the name of the contact, as it is in the roster
name = gajim.contacts.get_first_contact_from_jid(account, name = gajim.contacts.get_first_contact_from_jid(account,
receiver).get_shown_name() receiver).get_shown_name()
filename = os.path.basename(file_props['file-name']) filename = os.path.basename(file_props['file-name'])
if event_type == _('File Transfer Completed'): if event_type == _('File Transfer Completed'):
txt = _('You successfully sent %(filename)s to %(name)s.')\ txt = _('You successfully sent %(filename)s to %(name)s.')\
% {'filename': filename, 'name': name} % {'filename': filename, 'name': name}
img_name = 'gajim-ft_done' img_name = 'gajim-ft_done'
else: # ft stopped elif event_type == _('File Transfer Stopped'):
txt = _('File transfer of %(filename)s to %(name)s ' txt = _('File transfer of %(filename)s to %(name)s '
'stopped.') % {'filename': filename, 'name': name} 'stopped.') % {'filename': filename, 'name': name}
img_name = 'gajim-ft_stopped' img_name = 'gajim-ft_stopped'
else: # ft hash error
txt = _('File transfer of %(filename)s to %(name)s '
'failed.') % {'filename': filename, 'name': name}
img_name = 'gajim-ft_stopped'
path = gtkgui_helpers.get_icon_path(img_name, 48) path = gtkgui_helpers.get_icon_path(img_name, 48)
else: else:
txt = '' txt = ''
path = '' path = ''
if gajim.config.get('notify_on_file_complete') and \ if gajim.config.get('notify_on_file_complete') and \
(gajim.config.get('autopopupaway') or \ (gajim.config.get('autopopupaway') or \
gajim.connections[account].connected in (2, 3)): gajim.connections[account].connected in (2, 3)):
# we want to be notified and we are online/chat or we don't mind # we want to be notified and we are online/chat or we don't mind
# bugged when away/na/busy # bugged when away/na/busy
notify.popup(event_type, jid, account, msg_type, path_to_image=path, notify.popup(event_type, jid, account, msg_type, path_to_image=path,
@ -1103,6 +1161,23 @@ class Interface:
'resource. Please type a new one'), resource=proposed_resource, 'resource. Please type a new one'), resource=proposed_resource,
ok_handler=on_ok) ok_handler=on_ok)
def handle_event_jingleft_cancel(self, obj):
ft = self.instances['file_transfers']
file_props = None
# get the file_props of our session
for sid in obj.conn.files_props:
fp = obj.conn.files_props[sid]
if fp['session-sid'] == obj.sid:
file_props = fp
break
ft.set_status(file_props['type'], file_props['sid'], 'stop')
file_props['error'] = -4 # is it the right error code?
ft.show_stopped(obj.jid, file_props, 'Peer cancelled ' +
'the transfer')
def handle_event_jingle_incoming(self, obj): def handle_event_jingle_incoming(self, obj):
# ('JINGLE_INCOMING', account, peer jid, sid, tuple-of-contents==(type, # ('JINGLE_INCOMING', account, peer jid, sid, tuple-of-contents==(type,
# data...)) # data...))
@ -1440,6 +1515,7 @@ class Interface:
self.handle_event_jingle_disconnected], self.handle_event_jingle_disconnected],
'jingle-error-received': [self.handle_event_jingle_error], 'jingle-error-received': [self.handle_event_jingle_error],
'jingle-request-received': [self.handle_event_jingle_incoming], 'jingle-request-received': [self.handle_event_jingle_incoming],
'jingleFT-cancelled-received': [self.handle_event_jingleft_cancel],
'last-result-received': [self.handle_event_last_status_time], 'last-result-received': [self.handle_event_last_status_time],
'message-error': [self.handle_event_msgerror], 'message-error': [self.handle_event_msgerror],
'message-not-sent': [self.handle_event_msgnotsent], 'message-not-sent': [self.handle_event_msgnotsent],
@ -1498,7 +1574,7 @@ class Interface:
no_queue = len(gajim.events.get_events(account, jid)) == 0 no_queue = len(gajim.events.get_events(account, jid)) == 0
# type_ can be gc-invitation file-send-error file-error # type_ can be gc-invitation file-send-error file-error
# file-request-error file-request file-completed file-stopped # file-request-error file-request file-completed file-stopped
# jingle-incoming # file-hash-error jingle-incoming
# event_type can be in advancedNotificationWindow.events_list # event_type can be in advancedNotificationWindow.events_list
event_types = {'file-request': 'ft_request', event_types = {'file-request': 'ft_request',
'file-completed': 'ft_finished'} 'file-completed': 'ft_finished'}
@ -1627,7 +1703,7 @@ class Interface:
w = ctrl.parent_win w = ctrl.parent_win
elif type_ in ('normal', 'file-request', 'file-request-error', elif type_ in ('normal', 'file-request', 'file-request-error',
'file-send-error', 'file-error', 'file-stopped', 'file-completed', 'file-send-error', 'file-error', 'file-stopped', 'file-completed',
'jingle-incoming'): 'file-hash-error', 'jingle-incoming'):
# Get the first single message event # Get the first single message event
event = gajim.events.get_first_event(account, fjid, type_) event = gajim.events.get_first_event(account, fjid, type_)
if not event: if not event:

View file

@ -25,7 +25,7 @@ import message_control
from common import gajim from common import gajim
from common import helpers from common import helpers
from common.xmpp.protocol import NS_COMMANDS, NS_FILE, NS_MUC, NS_ESESSION from common.xmpp.protocol import NS_COMMANDS, NS_FILE, NS_MUC, NS_ESESSION, NS_JINGLE_FILE_TRANSFER
def build_resources_submenu(contacts, account, action, room_jid=None, def build_resources_submenu(contacts, account, action, room_jid=None,
room_account=None, cap=None): room_account=None, cap=None):
@ -227,7 +227,7 @@ control=None, gc_contact=None):
else: else:
start_chat_menuitem.connect('activate', start_chat_menuitem.connect('activate',
gajim.interface.on_open_chat_window, contact, account) gajim.interface.on_open_chat_window, contact, account)
if contact.supports(NS_FILE): if contact.supports(NS_FILE) or contact.supports(NS_JINGLE_FILE_TRANSFER):
send_file_menuitem.set_sensitive(True) send_file_menuitem.set_sensitive(True)
send_file_menuitem.connect('activate', send_file_menuitem.connect('activate',
roster.on_send_file_menuitem_activate, contact, account) roster.on_send_file_menuitem_activate, contact, account)

View file

@ -1926,6 +1926,9 @@ class RosterWindow:
ft.show_stopped(jid, data, error_msg=msg_err) ft.show_stopped(jid, data, error_msg=msg_err)
gajim.events.remove_events(account, jid, event) gajim.events.remove_events(account, jid, event)
return True return True
elif event.type_ == 'file-hash-error':
ft.show_hash_error(jid, data)
gajim.events.remove_events(account, jid, event)
elif event.type_ == 'file-completed': elif event.type_ == 'file-completed':
ft.show_completed(jid, data) ft.show_completed(jid, data)
gajim.events.remove_events(account, jid, event) gajim.events.remove_events(account, jid, event)

157
test/unit/test_jingle.py Normal file
View file

@ -0,0 +1,157 @@
'''
Tests for dispatcher_nb.py
'''
import unittest
import lib
lib.setup_env()
from mock import Mock
from common.protocol.bytestream import ConnectionIBBytestream, ConnectionSocks5Bytestream
from common.xmpp import dispatcher_nb
from common.xmpp import protocol
from common.jingle import ConnectionJingle
from common import gajim
from common.socks5 import SocksQueue
import common
session_init = '''
<iq xmlns="jabber:client" to="jingleft@thiessen.im/Gajim" type="set" id="43">
<jingle xmlns="urn:xmpp:jingle:1" action="session-initiate" initiator="jtest@thiessen.im/Gajim" sid="38">
<content name="fileWL1Y2JIPTM5RAD68" creator="initiator">
<security xmlns="urn:xmpp:jingle:security:xtls:0">
<method name="x509" />
</security>
<description xmlns="urn:xmpp:jingle:apps:file-transfer:1">
<offer>
<file xmlns="http://jabber.org/protocol/si/profile/file-transfer" name="to" size="2273">
<desc />
</file>
</offer>
</description>
<transport xmlns="urn:xmpp:jingle:transports:s5b:1" sid="39">
<candidate jid="jtest@thiessen.im/Gajim" cid="40" priority="8257536" host="192.168.2.100" type="direct" port="28011" />
<candidate jid="proxy.thiessen.im" cid="41" priority="655360" host="192.168.2.100" type="proxy" port="5000" />
<candidate jid="proxy.jabbim.cz" cid="42" priority="655360" host="192.168.2.100" type="proxy" port="7777" />
</transport>
</content>
</jingle>
</iq>
'''
transport_info = '''
<iq from='jtest@thiessen.im/Gajim'
id='hjdi8'
to='jingleft@thiessen.im/Gajim'
type='set'>
<jingle xmlns='urn:xmpp:jingle:1'
action='transport-info'
initiator='jtest@thiessen.im/Gajim'
sid='38'>
<content creator='initiator' name='fileWL1Y2JIPTM5RAD68'>
<transport xmlns='urn:xmpp:jingle:transports:s5b:1'
sid='vj3hs98y'>
<candidate-used cid='hr65dqyd'/>
</transport>
</content>
</jingle>
</iq>
'''
class Connection(Mock, ConnectionJingle, ConnectionSocks5Bytestream,
ConnectionIBBytestream):
def __init__(self):
Mock.__init__(self)
ConnectionJingle.__init__(self)
ConnectionSocks5Bytestream.__init__(self)
ConnectionIBBytestream.__init__(self)
self.connected = 2 # This tells gajim we are connected
def send(self, stanza=None, when=None):
# Called when gajim wants to send something
print str(stanza)
class TestJingle(unittest.TestCase):
def setUp(self):
self.dispatcher = dispatcher_nb.XMPPDispatcher()
gajim.nec = Mock()
gajim.socks5queue = SocksQueue(Mock())
# Setup mock client
self.client = Connection()
self.client.__str__ = lambda: 'Mock' # FIXME: why do I need this one?
self.client._caller = Connection()
self.client.defaultNamespace = protocol.NS_CLIENT
self.client.Connection = Connection() # mock transport
self.con = self.client.Connection
self.con.server_resource = None
self.con.connection = Connection()
'''
Fake file_props when we recieve a file. Gajim creates a file_props
out of a FileRequestRecieve event and from then on it changes in
a lot of places. It is easier to just copy it in here.
If the session_initiate stanza changes, this also must change.
'''
self.recieve_file = {'stream-methods':
'http://jabber.org/protocol/bytestreams',
'sender': u'jtest@thiessen.im/Gajim',
'file-name': u'test_recieved_file',
'request-id': u'43', 'sid': u'39',
'session-sid': u'38', 'session-type': 'jingle',
'transfered_size': [], 'receiver':
u'jingleft@thiessen.im/Gajim', 'desc': '',
u'size': u'2273', 'type': 'r',
'streamhosts': [{'initiator':
u'jtest@thiessen.im/Gajim',
'target': u'jingleft@thiessen.im/Gajim',
'cid': u'41', 'state': 0, 'host': u'192.168.2.100',
'type': u'direct', 'port': u'28011'},
{'initiator': u'jtest@thiessen.im/Gajim',
'target': u'jingleft@thiessen.im/Gajim',
'cid': u'42', 'state': 0, 'host': u'192.168.2.100',
'type': u'proxy', 'port': u'5000'}],
u'name': u'to'}
def tearDown(self):
# Unplug if needed
if hasattr(self.dispatcher, '_owner'):
self.dispatcher.PlugOut()
def _simulate_connect(self):
self.dispatcher.PlugIn(self.client) # client is owner
# Simulate that we have established a connection
self.dispatcher.StreamInit()
self.dispatcher.ProcessNonBlocking("<stream:stream xmlns:stream='http://etherx.jabber.org/streams' xmlns='jabber:client'>")
def _simulate_jingle_session(self):
self.dispatcher.RegisterHandler('iq', self.con._JingleCB, 'set'
, common.xmpp.NS_JINGLE)
self.dispatcher.ProcessNonBlocking(session_init)
session = self.con._sessions.values()[0] # The only session we have
jft = session.contents.values()[0] # jingleFT object
jft.file_props = self.recieve_file # We plug file_props manually
# The user accepts to recieve the file
# we have to manually simulate this behavior
session.approve_session()
self.con.send_file_approval(self.recieve_file)
self.dispatcher.ProcessNonBlocking(transport_info)
def test_jingle_session(self):
self._simulate_connect()
self._simulate_jingle_session()
if __name__ == '__main__':
unittest.main()

177
test/unit/test_socks5.py Normal file
View file

@ -0,0 +1,177 @@
'''
Tests for dispatcher_nb.py
'''
import unittest
import lib
lib.setup_env()
from mock import Mock
import sys
import socket
from common.socks5 import *
from common import jingle_xtls
class fake_sock(Mock):
def __init__(self, sockobj):
Mock.__init__(self)
self.sockobj = sockobj
def setup_stream(self):
sha1 = self.sockobj._get_sha1_auth()
self.incoming = []
self.incoming.append(self.sockobj._get_auth_response())
self.incoming.append(
self.sockobj._get_request_buff(sha1, 0x00)
)
self.outgoing = []
self.outgoing.append(self.sockobj._get_auth_buff())
self.outgoing.append(self.sockobj._get_request_buff(
sha1
))
def switch_stream(self):
# Roles are reversed, client will be expecting server stream
# and server will be expecting client stream
temp = self.incoming
self.incoming = self.outgoing
self.outgoing = temp
def _recv(self, foo):
return self.incoming.pop(0)
def _send(self, data):
# This method is surrounded by a try block,
# we can't use assert here
if data != self.outgoing[0]:
print 'FAILED SENDING TEST'
self.outgoing.pop(0)
class fake_idlequeue(Mock):
def __init__(self):
Mock.__init__(self)
def plug_idle(self, obj, writable=True, readable=True):
if readable:
obj.pollin()
if writable:
obj.pollout()
class TestSocks5(unittest.TestCase):
'''
Test class for Socks5
'''
def setUp(self):
streamhost = { 'host': None,
'port': 1,
'initiator' : None,
'target' : None}
queue = Mock()
queue.file_props = {}
#self.sockobj = Socks5Receiver(fake_idlequeue(), streamhost, None)
self.sockobj = Socks5Sender(fake_idlequeue(), None, 'server', Mock() ,
None, None, True, file_props={})
sock = fake_sock(self.sockobj)
self.sockobj._sock = sock
self.sockobj._recv = sock._recv
self.sockobj._send = sock._send
self.sockobj.state = 1
self.sockobj.connected = True
self.sockobj.pollend = self._pollend
# Something that the receiver needs
#self.sockobj.file_props['type'] = 'r'
# Something that the sender needs
self.sockobj.file_props = {}
self.sockobj.file_props['type'] = 'r'
self.sockobj.file_props['paused'] = ''
self.sockobj.queue = Mock()
self.sockobj.queue.process_result = self._pollend
def _pollend(self, foo = None, duu = None):
# This is a disconnect function
sys.exit("end of the road")
def _check_inout(self):
# Check if there isn't anything else to receive or send
sock = self.sockobj._sock
assert(sock.incoming == [])
assert(sock.outgoing == [])
def test_connection_server(self):
return
mocksock = self.sockobj._sock
mocksock.setup_stream()
#self.sockobj._sock.switch_stream()
s = socket.socket(2, 1, 6)
server = ('127.0.0.1', 28000)
s.connect(server)
s.send(mocksock.outgoing.pop(0))
self.assertEquals(s.recv(64), mocksock.incoming.pop(0))
s.send(mocksock.outgoing.pop(0))
self.assertEquals(s.recv(64), mocksock.incoming.pop(0))
def test_connection_client(self):
mocksock = self.sockobj._sock
mocksock.setup_stream()
mocksock.switch_stream()
s = socket.socket(10, 1, 6)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
netadd = ('::', 28000, 0, 0)
s.bind(netadd)
s.listen(socket.SOMAXCONN)
(s, address) = s.accept()
self.assertEquals(s.recv(64), mocksock.incoming.pop(0))
s.send(mocksock.outgoing.pop(0))
buff = s.recv(64)
inco = mocksock.incoming.pop(0)
#self.assertEquals(s.recv(64), mocksock.incoming.pop(0))
s.send(mocksock.outgoing.pop(0))
def test_client_negoc(self):
return
self.sockobj._sock.setup_stream()
try:
self.sockobj.pollout()
except SystemExit:
pass
self._check_inout()
def test_server_negoc(self):
return
self.sockobj._sock.setup_stream()
self.sockobj._sock.switch_stream()
try:
self.sockobj.idlequeue.plug_idle(self.sockobj, False, True)
except SystemExit:
pass
self._check_inout()
if __name__ == '__main__':
unittest.main()