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.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_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.connection_handlers_events import MessageOutgoingEvent
from common.exceptions import GajimGeneralException
@ -1722,13 +1722,13 @@ class ChatControl(ChatControlBase):
self._video_button.set_sensitive(self.video_available)
# 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._send_file_button.set_sensitive(True)
self._send_file_button.set_tooltip_text('')
else:
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(_(
"This contact does not support file transfer."))
else:

View File

@ -38,10 +38,10 @@ import logging
log = logging.getLogger('gajim.c.caps_cache')
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
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
NEW = 0

View File

@ -30,6 +30,7 @@ import stat
from common import gajim
import logger
from common import jingle_xtls
# DO NOT MOVE ABOVE OF import gajim
import sqlite3 as sqlite
@ -266,6 +267,8 @@ def check_and_possibly_create_paths():
MY_DATA = configpaths.gajimpaths['MY_DATA']
MY_CONFIG = configpaths.gajimpaths['MY_CONFIG']
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
@ -342,6 +345,17 @@ def check_and_possibly_create_paths():
print _('%s is a directory but should be a file') % CACHE_DB_PATH
print _('Gajim will now 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):
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',
'MY_EMOTS': u'emoticons', 'MY_ICONSETS': u'iconsets',
'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:
self.add(name, TYPE_DATA, windowsify(d[name]))
@ -151,6 +151,7 @@ class ConfigPaths:
self.add(name, TYPE_CACHE, windowsify(d[name]))
self.add('MY_CONFIG', TYPE_CONFIG, '')
self.add('MY_CERT', TYPE_CONFIG, '')
basedir = fse(os.environ.get(u'GAJIM_BASEDIR', defs.basedir))
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 exceptions
from common import dataforms
from common import jingle_xtls
from common.commands import ConnectionCommands
from common.pubsub import ConnectionPubSub
from common.pep import ConnectionPEP
@ -189,7 +190,10 @@ class ConnectionDisco:
query.setAttr('node', 'http://gajim.org#' + gajim.version.split('-', 1)[
0])
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.setAttr('var', f)
query.addChild(node=feature)
@ -1979,10 +1983,37 @@ ConnectionJingle, ConnectionIBBytestream):
gajim.nec.push_incoming_event(SearchFormReceivedEvent(None,
conn=self, stanza=iq_obj))
def _StreamCB(self, con, iq_obj):
log.debug('StreamCB')
gajim.nec.push_incoming_event(StreamReceivedEvent(None,
conn=self, stanza=iq_obj))
def _search_fields_received(self, con, iq_obj):
jid = jid = helpers.get_jid_from_iq(iq_obj)
tag = iq_obj.getTag('query', namespace = common.xmpp.NS_SEARCH)
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):
# 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('presence', self._StanzaArrivedCB)
con.RegisterHandler('message', self._StanzaArrivedCB)
con.RegisterHandler('unknown', self._StreamCB,
common.xmpp.NS_XMPP_STREAMS, xmlns=common.xmpp.NS_STREAMS)
con.RegisterHandler('unknown', self._StreamCB, 'urn:ietf:params:xml:ns:xmpp-streams', xmlns='http://etherx.jabber.org/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.pep import SUPPORTED_PERSONAL_USER_EVENTS
from common.xmpp.protocol import NS_CHATSTATES
from common.jingle_transport import JingleTransportSocks5
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.sid = self.jingle_session.sid
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):
name = 'jingle-error-received'
@ -1901,6 +1912,10 @@ class FileRequestReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
name = 'file-request-received'
base_network_events = []
def init(self):
self.jingle_content = None
self.FT_content = None
def generate(self):
self.get_id()
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['sender'] = self.fjid
self.file_props['request-id'] = self.id_
si = self.stanza.getTag('si')
profile = si.getAttr('profile')
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
if self.jingle_content:
self.file_props['session-type'] = 'jingle'
self.file_props['stream-methods'] = xmpp.NS_BYTESTREAM
file_tag = self.jingle_content.getTag('description').getTag(
'offer').getTag('file')
else:
self.conn.send_file_rejection(self.file_props, code='400', typ='stream')
raise xmpp.NodeProcessed
file_tag = si.getTag('file')
si = self.stanza.getTag('si')
profile = si.getAttr('profile')
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():
if attribute in ('name', 'size', 'hash', 'date'):
val = file_tag.getAttr(attribute)
@ -1940,13 +1963,41 @@ class FileRequestReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
if file_desc_tag is not None:
self.file_props['desc'] = file_desc_tag.getData()
mime_type = si.getAttr('mime-type')
if mime_type is not None:
self.file_props['mime-type'] = mime_type
if not self.jingle_content:
mime_type = si.getAttr('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['sid'] = unicode(si.getAttr('id'))
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
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_ACTIVITY_ICONSETS_PATH = gajimpaths['MY_ACTIVITY_ICONSETS']
MY_CACERTS = gajimpaths['MY_CACERTS']
MY_PEER_CERTS_PATH = gajimpaths['MY_PEER_CERTS']
TMP = gajimpaths['TMP']
DATA_DIR = gajimpaths['DATA']
ICONS_DIR = gajimpaths['ICONS']
@ -69,6 +70,7 @@ HOME_DIR = gajimpaths['HOME']
PLUGINS_DIRS = [gajimpaths['PLUGINS_BASE'],
gajimpaths['PLUGINS_USER']]
PLUGINS_CONFIG_DIR = gajimpaths['PLUGINS_CONFIG_DIR']
MY_CERT_DIR = gajimpaths['MY_CERT']
try:
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,
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,
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
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_VIDEO)
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.gajim_common_features + gajim.gajim_optional_features[a])
# re-send presence with new hash

View File

@ -37,7 +37,11 @@ import gajim
from jingle_session import JingleSession, JingleStates
if gajim.HAVE_FARSTREAM:
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):
"""
@ -75,27 +79,38 @@ class ConnectionJingle(object):
"""
# get data
jid = helpers.get_full_jid_from_iq(stanza)
id = stanza.getID()
id_ = stanza.getID()
if (jid, id) in self.__iq_responses.keys():
self.__iq_responses[(jid, id)].on_stanza(stanza)
del self.__iq_responses[(jid, id)]
if (jid, id_) in self.__iq_responses.keys():
self.__iq_responses[(jid, id_)].on_stanza(stanza)
del self.__iq_responses[(jid, id_)]
raise xmpp.NodeProcessed
jingle = stanza.getTag('jingle')
if not jingle: return
sid = jingle.getAttr('sid')
# a jingle element is not necessary in iq-result stanza
# 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
if sid not in self._sessions:
#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
# we already have such session in dispatcher...
self._sessions[sid].collect_iq_id(id_)
self._sessions[sid].on_stanza(stanza)
# 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)
raise xmpp.NodeProcessed
@ -126,16 +141,55 @@ class ConnectionJingle(object):
jingle.start_session()
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):
if sid:
return (session for session in self._sessions.values() if session.sid == sid)
sessions = (session for session in self._sessions.values() if session.peerjid == jid)
return (session for session in self._sessions.values() if \
session.sid == sid)
sessions = (session for session in self._sessions.values() if \
session.peerjid == jid)
if media:
if media not in ('audio', 'video'):
if media not in ('audio', 'video', 'file'):
return tuple()
else:
return (session for session in sessions if session.get_content(media))
return (session for session in sessions if \
session.get_content(media))
else:
return sessions
@ -147,6 +201,8 @@ class ConnectionJingle(object):
else:
return None
elif media:
if media not in ('audio', 'video', 'file'):
return None
for session in self._sessions.values():
if session.peerjid == jid and session.get_content(media):
return session

View File

@ -16,6 +16,7 @@ Handles Jingle contents (XEP 0166)
"""
import xmpp
from jingle_transport import JingleTransportIBB
contents = {}
@ -69,7 +70,7 @@ class JingleContent(object):
'session-initiate': [self.__on_transport_info],
'session-terminate': [],
'transport-info': [self.__on_transport_info],
'transport-replace': [],
'transport-replace': [self.__on_transport_replace],
'transport-accept': [],
'transport-reject': [],
'iq-result': [],
@ -99,7 +100,7 @@ class JingleContent(object):
"""
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):
"""
@ -109,12 +110,15 @@ class JingleContent(object):
for callback in self.callbacks[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):
"""
Got a new transport candidate
"""
candidates = self.transport.parse_transport_stanza(
content.getTag('transport'))
content.getTag('transport'))
if candidates:
self.add_remote_candidates(candidates)
@ -134,6 +138,17 @@ class JingleContent(object):
content.addChild(node=self.transport.make_transport([candidate]))
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):
content = self.__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):
def __init__(self, session, media, transport=None):
if transport is None:
transport = JingleTransportICEUDP()
transport = JingleTransportICEUDP(None)
JingleContent.__init__(self, session, transport)
self.media = media
self._dtmf_running = False

View File

@ -28,9 +28,13 @@ Handles Jingle sessions (XEP 0166)
import gajim #Get rid of that?
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 JingleContent
from jingle_ft import STATE_TRANSPORT_REPLACE
from common.connection_handlers_events import *
import logging
log = logging.getLogger("gajim.c.jingle_session")
# FIXME: Move it to JingleSession.States?
class JingleStates(object):
@ -59,7 +63,7 @@ class JingleSession(object):
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,
weinitiate -- boolean, are we the initiator?
@ -85,8 +89,21 @@ class JingleSession(object):
sid = con.connection.getAnID()
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
# use .prepend() to add new callbacks, especially when you're going
# to send error instead of ack
@ -101,7 +118,7 @@ class JingleSession(object):
'description-info': [self.__broadcast, self.__ack], #TODO
'security-info': [self.__ack], #TODO
'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-initiate': [self.__on_session_initiate, self.__broadcast,
self.__ack],
@ -111,9 +128,14 @@ class JingleSession(object):
'transport-replace': [self.__broadcast, self.__on_transport_replace], #TODO
'transport-accept': [self.__ack], #TODO
'transport-reject': [self.__ack], #TODO
'iq-result': [],
'iq-result': [self.__broadcast],
'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):
"""
@ -128,15 +150,23 @@ class JingleSession(object):
reason = xmpp.Node('reason')
reason.addChild('decline')
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):
content = self.get_content(media)
def approve_content(self, media, name=None):
content = self.get_content(media, name)
if content:
content.accepted = True
self.on_session_state_changed(content)
def reject_content(self, media):
content = self.get_content(media)
def reject_content(self, media, name=None):
content = self.get_content(media, name)
if content:
if self.state == JingleStates.active:
self.__content_reject(content)
@ -154,13 +184,14 @@ class JingleSession(object):
reason.addChild('cancel')
self._session_terminate(reason)
def get_content(self, media=None):
def get_content(self, media=None, name=None):
if media is None:
return
for content in self.contents.values():
if content.media == media:
return content
if name is None or content.name == name:
return content
def add_content(self, name, content, creator='we'):
"""
@ -195,12 +226,22 @@ class JingleSession(object):
content = self.contents[(creator, name)]
self.__content_remove(content, reason)
self.contents[(creator, name)].destroy()
if not self.contents:
self.end_session()
def modify_content(self, creator, name, *someother):
"""
We do not need this now
"""
pass
def modify_content(self, creator, name, transport = None):
'''
Currently used for transport replacement
'''
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):
if self.state == JingleStates.ended:
@ -216,10 +257,15 @@ class JingleSession(object):
elif content and self.weinitiate:
self.__content_accept(content)
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:
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.
self.__content_add(content)
else:
@ -261,6 +307,7 @@ class JingleSession(object):
stanza, jingle = self.__make_jingle('transport-info')
jingle.addChild(node=content)
self.connection.connection.send(stanza)
self.collect_iq_id(stanza.getID())
def send_description_info(self, content):
assert self.state != JingleStates.ended
@ -286,7 +333,7 @@ class JingleSession(object):
self.__send_error(stanza, 'bad-request')
return
# 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')
return
else:
@ -311,6 +358,7 @@ class JingleSession(object):
Default callback for action stanzas -- simple ack and stop processing
"""
response = stanza.buildReply('result')
response.delChild(response.getQuery())
self.connection.connection.send(response)
def __on_error(self, stanza, jingle, error, action):
@ -325,18 +373,42 @@ class JingleSession(object):
error_name = child.getName()
self.__dispatch_error(error_name, text, error.getAttr('type'))
# 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):
for content in jingle.iterTags('content'):
creator = content['creator']
name = content['name']
if (creator, name) in self.contents:
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...
# What was the previous transport?!?
# Anyway, content's transport is not modifiable yet
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:
stanza, jingle = self.__make_jingle('transport-reject')
content = jingle.setTag('content', attrs={'creator': creator,
@ -357,9 +429,22 @@ class JingleSession(object):
def __on_session_info(self, stanza, jingle, error, action):
# TODO: ringing, active, (un)hold, (un)mute
payload = jingle.getPayload()
if payload:
self.__send_error(stanza, 'feature-not-implemented', 'unsupported-info', type_='modify')
raise xmpp.NodeProcessed
for p in payload:
if p.getName() == 'checksum':
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):
for content in jingle.iterTags('content'):
@ -382,6 +467,7 @@ class JingleSession(object):
if self.state != JingleStates.pending:
raise OutOfOrder
self.state = JingleStates.active
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
# Jingle with unknown entities, it SHOULD return a <service-unavailable/>
# 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
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 not contents:
@ -462,6 +554,17 @@ class JingleSession(object):
"""
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'):
name = content['name']
creator = content['creator']
@ -483,9 +586,11 @@ class JingleSession(object):
else:
# TODO
text = reason
gajim.nec.push_incoming_event(JingleDisconnectedReceivedEvent(None,
conn=self.connection, jingle_session=self, media=None,
reason=text))
if reason == 'cancel' and self.session_type_FT:
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):
"""
@ -502,6 +607,8 @@ class JingleSession(object):
for element in jingle.iterTags('content'):
transport = get_jingle_transport(element.getTag('transport'))
if transport:
transport.ourjid = self.ourjid
content_type = get_jingle_content(element.getTag('description'))
if content_type:
try:
@ -531,12 +638,15 @@ class JingleSession(object):
return (contents, contents_rejected, failure_reason)
def __dispatch_error(self, error=None, text=None, type_=None):
if text:
text = '%s (%s)' % (error, text)
if type_ != 'modify':
gajim.nec.push_incoming_event(JingleErrorReceivedEvent(None,
conn=self.connection, jingle_session=self,
reason=text or error))
def __reason_from_stanza(self, stanza):
# TODO: Move to GUI?
@ -557,13 +667,12 @@ class JingleSession(object):
return (reason, text)
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,
'sid': self.sid}
if action == 'session-initiate':
attrs['initiator'] = self.initiator
elif action == 'session-accept':
attrs['responder'] = self.responder
'sid': self.sid,
'initiator' : self.initiator}
jingle = stanza.addChild('jingle', attrs=attrs, namespace=xmpp.NS_JINGLE)
if reason is not None:
jingle.addChild(node=reason)
@ -605,6 +714,7 @@ class JingleSession(object):
self.__append_contents(jingle)
self.__broadcast(stanza, jingle, None, 'session-initiate-sent')
self.connection.connection.send(stanza)
self.collect_iq_id(stanza.getID())
self.state = JingleStates.pending
def __session_accept(self):
@ -613,6 +723,7 @@ class JingleSession(object):
self.__append_contents(jingle)
self.__broadcast(stanza, jingle, None, 'session-accept-sent')
self.connection.connection.send(stanza)
self.collect_iq_id(stanza.getID())
self.state = JingleStates.active
def __session_info(self, payload=None):
@ -621,9 +732,15 @@ class JingleSession(object):
if payload:
jingle.addChild(node=payload)
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):
assert self.state != JingleStates.ended
stanza, jingle = self.__make_jingle('session-terminate', reason=reason)
self.__broadcast_all(stanza, jingle, None, 'session-terminate-sent')
if self.connection.connection and self.connection.connected >= 2:
@ -647,7 +764,8 @@ class JingleSession(object):
stanza, jingle = self.__make_jingle('content-add')
self.__append_content(jingle, content)
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):
# TODO: test
@ -655,7 +773,8 @@ class JingleSession(object):
stanza, jingle = self.__make_jingle('content-accept')
self.__append_content(jingle, content)
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):
assert self.state != JingleStates.ended

View File

@ -16,21 +16,29 @@ Handles Jingle Transports (currently only ICE-UDP)
"""
import xmpp
import socket
from common import gajim
from common.protocol.bytestream import ConnectionSocks5Bytestream
import logging
log = logging.getLogger('gajim.c.jingle_transport')
transports = {}
def get_jingle_transport(node):
namespace = node.getNamespace()
if namespace in transports:
return transports[namespace]()
return transports[namespace](node)
class TransportType(object):
"""
Possible types of a JingleTransport
"""
datagram = 1
streaming = 2
ICEUDP = 1
SOCKS5 = 2
IBB = 3
class JingleTransport(object):
@ -70,16 +78,258 @@ class JingleTransport(object):
Return the list of transport candidates from a transport stanza
"""
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:
import farstream
except Exception:
pass
class JingleTransportICEUDP(JingleTransport):
def __init__(self):
JingleTransport.__init__(self, TransportType.datagram)
def __init__(self, node):
JingleTransport.__init__(self, TransportType.ICEUDP)
def make_candidate(self, candidate):
types = {farstream.CANDIDATE_TYPE_HOST: 'host',
@ -149,3 +399,5 @@ class JingleTransportICEUDP(JingleTransport):
return candidates
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 helpers
from common import dataforms
from common.connection_handlers_events import FileRequestReceivedEvent, \
FileRequestErrorEvent, InformationEvent
from common import ged
from common import jingle_xtls
from common.socks5 import Socks5Receiver
@ -140,6 +139,34 @@ class ConnectionBytestream:
# user response to ConfirmationDialog may come after we've disconneted
if not self.connection or self.connected < 2:
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.setAttr('id', file_props['request-id'])
si = iq.setTag('si', namespace=xmpp.NS_SI)
@ -168,6 +195,10 @@ class ConnectionBytestream:
# user response to ConfirmationDialog may come after we've disconneted
if not self.connection or self.connected < 2:
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.setAttr('id', file_props['request-id'])
if code == '400' and typ in ('stream', 'profile'):
@ -221,6 +252,7 @@ class ConnectionBytestream:
raise xmpp.NodeProcessed
def _siSetCB(self, con, iq_obj):
from common.connection_handlers_events import FileRequestReceivedEvent
gajim.nec.push_incoming_event(FileRequestReceivedEvent(None, conn=self,
stanza=iq_obj))
raise xmpp.NodeProcessed
@ -240,6 +272,7 @@ class ConnectionBytestream:
return
jid = self._ft_get_from(iq_obj)
file_props['error'] = -3
from common.connection_handlers_events import FileRequestErrorEvent
gajim.nec.push_incoming_event(FileRequestErrorEvent(None, conn=self,
jid=jid, file_props=file_props, error_msg=''))
raise xmpp.NodeProcessed
@ -273,6 +306,8 @@ class ConnectionSocks5Bytestream(ConnectionBytestream):
if contact.get_full_jid() == receiver_jid:
file_props['error'] = -5
self.remove_transfer(file_props)
from common.connection_handlers_events import \
FileRequestErrorEvent
gajim.nec.push_incoming_event(FileRequestErrorEvent(None,
conn=self, jid=contact.jid, file_props=file_props,
error_msg=''))
@ -332,9 +367,10 @@ class ConnectionSocks5Bytestream(ConnectionBytestream):
port = gajim.config.get('file_transfers_port')
listener = gajim.socks5queue.start_listener(port, sha_str,
self._result_socks5_sid, file_props['sid'])
self._result_socks5_sid, file_props)
if not listener:
file_props['error'] = -5
from common.connection_handlers_events import FileRequestErrorEvent
gajim.nec.push_incoming_event(FileRequestErrorEvent(None, conn=self,
jid=unicode(receiver), file_props=file_props, error_msg=''))
self._connect_error(unicode(receiver), file_props['sid'],
@ -374,6 +410,7 @@ class ConnectionSocks5Bytestream(ConnectionBytestream):
port = gajim.config.get('file_transfers_port')
self._add_streamhosts_to_query(query, sender, port, my_ips)
except socket.gaierror:
from common.connection_handlers_events import InformationEvent
gajim.nec.push_incoming_event(InformationEvent(None, conn=self,
level='error', pri_txt=_('Wrong host'),
sec_txt=_('Invalid local address? :-O')))
@ -546,6 +583,8 @@ class ConnectionSocks5Bytestream(ConnectionBytestream):
if file_props is not None:
self.disconnect_transfer(file_props)
file_props['error'] = -3
from common.connection_handlers_events import \
FileRequestErrorEvent
gajim.nec.push_incoming_event(FileRequestErrorEvent(None,
conn=self, jid=to, file_props=file_props, error_msg=msg))
@ -578,6 +617,7 @@ class ConnectionSocks5Bytestream(ConnectionBytestream):
return
file_props = self.files_props[id_]
file_props['error'] = -4
from common.connection_handlers_events import FileRequestErrorEvent
gajim.nec.push_incoming_event(FileRequestErrorEvent(None, conn=self,
jid=jid, file_props=file_props, error_msg=''))
raise xmpp.NodeProcessed
@ -713,7 +753,10 @@ class ConnectionSocks5Bytestream(ConnectionBytestream):
raise xmpp.NodeProcessed
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:
fasts = file_props['fast']
if len(fasts) > 0:
@ -741,9 +784,9 @@ class ConnectionIBBytestream(ConnectionBytestream):
elif typ == 'set' and stanza.getTag('close', namespace=xmpp.NS_IBB):
self.StreamCloseHandler(conn, stanza)
elif typ == 'result':
self.StreamCommitHandler(conn, stanza)
self.SendHandler()
elif typ == 'error':
self.StreamOpenReplyHandler(conn, stanza)
gajim.socks5queue.error_cb()
else:
conn.send(xmpp.Error(stanza, xmpp.ERR_BAD_REQUEST))
raise xmpp.NodeProcessed
@ -918,13 +961,17 @@ class ConnectionIBBytestream(ConnectionBytestream):
log.debug('StreamCloseHandler called sid->%s' % sid)
# look in sending files
if sid in self.files_props.keys():
conn.send(stanza.buildReply('result'))
gajim.socks5queue.complete_transfer_cb(self.name, file_props)
reply = stanza.buildReply('result')
reply.delChild('close')
conn.send(reply)
gajim.socks5queue.complete_transfer_cb(self.name, self.files_props[sid])
del self.files_props[sid]
# look in receiving files
elif 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()
gajim.socks5queue.complete_transfer_cb(self.name, file_props)
gajim.socks5queue.remove_file_props(self.name, sid)
@ -964,6 +1011,7 @@ class ConnectionIBBytestream(ConnectionBytestream):
if stanza.getTag('data'):
if self.IBBMessageHandler(conn, stanza):
reply = stanza.buildReply('result')
reply.delChild('data')
conn.send(reply)
raise xmpp.NodeProcessed
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
import time
import string
import hashlib
def ascii_upper(s):
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_AUDIO = 'urn:xmpp:jingle:apps:rtp:audio' # 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_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_LOCATION = 'http://jabber.org/protocol/geoloc' # XEP-0080
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_XMPP_STREAMS = 'urn:ietf:params:xml:ns:xmpp-streams'
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_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 = '''
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.
@ -1024,13 +1037,84 @@ class Iq(Protocol):
attrs={'id': self.getID()})
iq.setQuery(self.getQuery().getName()).setNamespace(self.getQueryNS())
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):
"""
Acknowledgement elements for Stream Management
"""
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)
def buildAnswer(self, handled):
@ -1407,3 +1491,4 @@ class DataForm(Node):
Simple dictionary interface for setting datafields values by their names
"""
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,
on_response_no=None):
on_response_no=None, type_=gtk.MESSAGE_QUESTION):
self.user_response_yes = on_response_yes
self.user_response_no = on_response_no
if hasattr(gajim.interface, 'roster') and gajim.interface.roster:
parent = gajim.interface.roster.window
else:
parent = None
HigDialog.__init__(self, parent, gtk.MESSAGE_QUESTION,
gtk.BUTTONS_YES_NO, pritext, sectext,
on_response_yes=self.on_response_yes,
HigDialog.__init__(self, parent, type_, gtk.BUTTONS_YES_NO, pritext,
sectext, on_response_yes=self.on_response_yes,
on_response_no=self.on_response_no)
if checktext:

View File

@ -35,6 +35,10 @@ from common import gajim
from common import helpers
from common.protocol.bytestream import (is_transfer_active, is_transfer_paused,
is_transfer_stopped)
from common.xmpp.protocol import NS_JINGLE_FILE_TRANSFER
import logging
log = logging.getLogger('gajim.filetransfer_window')
C_IMAGE = 0
C_LABELS = 1
@ -42,7 +46,8 @@ C_FILE = 2
C_TIME = 3
C_PROGRESS = 4
C_PERCENT = 5
C_SID = 6
C_PULSE = 6
C_SID = 7
class FileTransfersWindow:
@ -61,7 +66,8 @@ class FileTransfersWindow:
shall_notify = gajim.config.get('notify_on_file_complete')
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)
col = gtk.TreeViewColumn()
@ -108,6 +114,7 @@ class FileTransfersWindow:
col.pack_start(renderer, expand=False)
col.add_attribute(renderer, 'text', C_PROGRESS)
col.add_attribute(renderer, 'value', C_PERCENT)
col.add_attribute(renderer, 'pulse', C_PULSE)
col.set_resizable(True)
col.set_expand(False)
self.tree.append_column(col)
@ -121,6 +128,8 @@ class FileTransfersWindow:
'pause': gtk.STOCK_MEDIA_PAUSE,
'continue': gtk.STOCK_MEDIA_PLAY,
'ok': gtk.STOCK_APPLY,
'computing': gtk.STOCK_EXECUTE,
'hash_error': gtk.STOCK_STOP,
}
self.tree.get_selection().set_mode(gtk.SELECTION_SINGLE)
@ -240,6 +249,21 @@ class FileTransfersWindow:
dialogs.ErrorDialog(_('File transfer stopped'), sectext)
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):
win = gtk.ScrolledWindow()
win.set_shadow_type(gtk.SHADOW_IN)
@ -310,8 +334,17 @@ class FileTransfersWindow:
file_path, file_name, file_desc)
if file_props is None:
return False
self.add_transfer(account, contact, file_props)
gajim.connections[account].send_file_request(file_props)
if contact.supports(NS_JINGLE_FILE_TRANSFER):
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
def _start_receive(self, file_path, account, contact, file_props):
@ -436,6 +469,36 @@ class FileTransfersWindow:
file_props['stopped'] = True
elif status == 'ok':
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))
path = self.model.get_path(iter_)
self.select_func(path)
@ -576,7 +639,12 @@ class FileTransfersWindow:
status = 'stop'
self.model.set(iter_, 0, self.get_icon(status))
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:
path = self.model.get_path(iter_)
self.select_func(path)
@ -642,7 +710,7 @@ class FileTransfersWindow:
file_name = file_props['name']
text_props = gobject.markup_escape_text(file_name) + '\n'
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'])
self.set_progress(file_props['type'], file_props['sid'], 0, iter_)
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
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
##
import os
import sys
import re
@ -71,6 +70,7 @@ from session import ChatControlSession
import common.sleepy
from common.xmpp import idlequeue
from common.xmpp import Hashes
from common.zeroconf import connection_zeroconf
from common import resolver
from common import caps_cache
@ -83,6 +83,7 @@ from common import logging_helpers
from common.connection_handlers_events import OurShowEvent, \
FileRequestErrorEvent, InformationEvent
from common.connection import Connection
from common import jingle
import roster_window
import profile_window
@ -918,21 +919,65 @@ class Interface:
self.instances['file_transfers'].set_progress(file_props['type'],
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):
ft = self.instances['file_transfers']
if file_props['error'] == 0:
ft.set_progress(file_props['type'], file_props['sid'],
file_props['received-len'])
file_props['received-len'])
else:
ft.set_status(file_props['type'], file_props['sid'], 'stop')
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
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
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 file_props['error'] == 0:
if gajim.config.get('notify_on_file_complete'):
@ -943,6 +988,8 @@ class Interface:
elif file_props['error'] == -6:
ft.show_stopped(jid, file_props,
error_msg=_('Error opening file'))
elif file_props['error'] == -10:
ft.show_hash_error(jid, file_props)
return
msg_type = ''
@ -954,6 +1001,9 @@ class Interface:
elif file_props['error'] in (-1, -6):
msg_type = 'file-stopped'
event_type = _('File Transfer Stopped')
elif file_props['error'] == -10:
msg_type = 'file-hash-error'
event_type = _('File Transfer Failed')
if event_type == '':
# 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
sender = unicode(file_props['sender']).split('/')[0]
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'])
if event_type == _('File Transfer Completed'):
txt = _('You successfully received %(filename)s from '
'%(name)s.') % {'filename': filename, 'name': name}
img_name = 'gajim-ft_done'
else: # ft stopped
elif event_type == _('File Transfer Stopped'):
txt = _('File transfer of %(filename)s from %(name)s '
'stopped.') % {'filename': filename, 'name': name}
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:
receiver = file_props['receiver']
if hasattr(receiver, 'jid'):
@ -988,24 +1042,28 @@ class Interface:
receiver = receiver.split('/')[0]
# get the name of the contact, as it is in the roster
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'])
if event_type == _('File Transfer Completed'):
txt = _('You successfully sent %(filename)s to %(name)s.')\
% {'filename': filename, 'name': name}
% {'filename': filename, 'name': name}
img_name = 'gajim-ft_done'
else: # ft stopped
elif event_type == _('File Transfer Stopped'):
txt = _('File transfer of %(filename)s to %(name)s '
'stopped.') % {'filename': filename, 'name': name}
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)
else:
txt = ''
path = ''
if gajim.config.get('notify_on_file_complete') and \
(gajim.config.get('autopopupaway') or \
gajim.connections[account].connected in (2, 3)):
(gajim.config.get('autopopupaway') or \
gajim.connections[account].connected in (2, 3)):
# we want to be notified and we are online/chat or we don't mind
# bugged when away/na/busy
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,
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):
# ('JINGLE_INCOMING', account, peer jid, sid, tuple-of-contents==(type,
# data...))
@ -1440,6 +1515,7 @@ class Interface:
self.handle_event_jingle_disconnected],
'jingle-error-received': [self.handle_event_jingle_error],
'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],
'message-error': [self.handle_event_msgerror],
'message-not-sent': [self.handle_event_msgnotsent],
@ -1498,7 +1574,7 @@ class Interface:
no_queue = len(gajim.events.get_events(account, jid)) == 0
# type_ can be gc-invitation file-send-error file-error
# 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_types = {'file-request': 'ft_request',
'file-completed': 'ft_finished'}
@ -1627,7 +1703,7 @@ class Interface:
w = ctrl.parent_win
elif type_ in ('normal', 'file-request', 'file-request-error',
'file-send-error', 'file-error', 'file-stopped', 'file-completed',
'jingle-incoming'):
'file-hash-error', 'jingle-incoming'):
# Get the first single message event
event = gajim.events.get_first_event(account, fjid, type_)
if not event:

View File

@ -25,7 +25,7 @@ import message_control
from common import gajim
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,
room_account=None, cap=None):
@ -227,7 +227,7 @@ control=None, gc_contact=None):
else:
start_chat_menuitem.connect('activate',
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.connect('activate',
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)
gajim.events.remove_events(account, jid, event)
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':
ft.show_completed(jid, data)
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()