diff --git a/src/chat_control.py b/src/chat_control.py index b16c0d109..d2201eb91 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -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: diff --git a/src/common/caps_cache.py b/src/common/caps_cache.py index b6853db64..33bdb6afd 100644 --- a/src/common/caps_cache.py +++ b/src/common/caps_cache.py @@ -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 diff --git a/src/common/check_paths.py b/src/common/check_paths.py index e1c3d3b8a..3250f46a4 100644 --- a/src/common/check_paths.py +++ b/src/common/check_paths.py @@ -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) diff --git a/src/common/configpaths.py b/src/common/configpaths.py index ad00d431a..93c2b996b 100644 --- a/src/common/configpaths.py +++ b/src/common/configpaths.py @@ -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'))) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 4c65f66bc..8a537e15e 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -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) + diff --git a/src/common/connection_handlers_events.py b/src/common/connection_handlers_events.py index bddac1143..d08783817 100644 --- a/src/common/connection_handlers_events.py +++ b/src/common/connection_handlers_events.py @@ -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): diff --git a/src/common/gajim.py b/src/common/gajim.py index db1a4e6c5..1a1c84e83 100644 --- a/src/common/gajim.py +++ b/src/common/gajim.py @@ -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 = {} diff --git a/src/common/helpers.py b/src/common/helpers.py index a98110009..a4ce133f3 100644 --- a/src/common/helpers.py +++ b/src/common/helpers.py @@ -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 diff --git a/src/common/jingle.py b/src/common/jingle.py index aec85b9cc..4c2598487 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -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 diff --git a/src/common/jingle_content.py b/src/common/jingle_content.py index e2a5c9a28..079000f85 100644 --- a/src/common/jingle_content.py +++ b/src/common/jingle_content.py @@ -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) diff --git a/src/common/jingle_ft.py b/src/common/jingle_ft.py new file mode 100644 index 000000000..1e0564409 --- /dev/null +++ b/src/common/jingle_ft.py @@ -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 . +## + + +""" +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 diff --git a/src/common/jingle_ftstates.py b/src/common/jingle_ftstates.py new file mode 100644 index 000000000..271f53927 --- /dev/null +++ b/src/common/jingle_ftstates.py @@ -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() diff --git a/src/common/jingle_rtp.py b/src/common/jingle_rtp.py index e0de6bb14..4e552dbb1 100644 --- a/src/common/jingle_rtp.py +++ b/src/common/jingle_rtp.py @@ -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 diff --git a/src/common/jingle_session.py b/src/common/jingle_session.py index a56dc762b..b3faafe2d 100644 --- a/src/common/jingle_session.py +++ b/src/common/jingle_session.py @@ -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 # 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 diff --git a/src/common/jingle_transport.py b/src/common/jingle_transport.py index aa1ce54bb..ff736f718 100644 --- a/src/common/jingle_transport.py +++ b/src/common/jingle_transport.py @@ -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 diff --git a/src/common/jingle_xtls.py b/src/common/jingle_xtls.py new file mode 100644 index 000000000..edff95355 --- /dev/null +++ b/src/common/jingle_xtls.py @@ -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 . +## + +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') diff --git a/src/common/protocol/bytestream.py b/src/common/protocol/bytestream.py index 5693eb460..b370ec266 100644 --- a/src/common/protocol/bytestream.py +++ b/src/common/protocol/bytestream.py @@ -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: diff --git a/src/common/socks5.py b/src/common/socks5.py index 3ec2cfb4e..89402e898 100644 --- a/src/common/socks5.py +++ b/src/common/socks5.py @@ -35,6 +35,15 @@ from errno import EISCONN from errno import EINPROGRESS from errno import EAFNOSUPPORT from xmpp.idlequeue import IdleObject + +import jingle_xtls + +if jingle_xtls.PYOPENSSL_PRESENT: + import OpenSSL + +import logging +log = logging.getLogger('gajim.c.socks5') + MAX_BUFF_LEN = 65536 # after foo seconds without activity label transfer as 'stalled' @@ -58,7 +67,7 @@ class SocksQueue: """ def __init__(self, idlequeue, complete_transfer_cb=None, - progress_transfer_cb=None, error_cb=None): + progress_transfer_cb=None, error_cb=None): self.connected = 0 self.readers = {} self.files_props = {} @@ -72,27 +81,36 @@ class SocksQueue: self.complete_transfer_cb = complete_transfer_cb self.progress_transfer_cb = progress_transfer_cb self.error_cb = error_cb - self.on_success = None - self.on_failure = None + self.on_success = {} # {id: cb} + self.on_failure = {} # {id: cb} - def start_listener(self, port, sha_str, sha_handler, sid): + def start_listener(self, port, sha_str, sha_handler, fp, fingerprint=None, + type='sender'): """ Start waiting for incomming connections on (host, port) and do a socks5 authentication using sid for generated SHA """ + sid = fp['sid'] + self.type = type # It says whether we are sending or receiving self.sha_handlers[sha_str] = (sha_handler, sid) - if self.listener is None: - self.listener = Socks5Listener(self.idlequeue, port) + if self.listener is None or self.listener.connections == []: + self.listener = Socks5Listener(self.idlequeue, port, fp, + fingerprint=fingerprint) self.listener.queue = self self.listener.bind() if self.listener.started is False: self.listener = None # We cannot bind port, call error callback and fail self.error_cb(_('Unable to bind to port %s.') % port, - _('Maybe you have another running instance of Gajim. File ' - 'Transfer will be cancelled.')) + _('Maybe you have another running instance of Gajim. File ' + 'Transfer will be cancelled.')) return None - self.connected += 1 + else: + # There is already a listener, we update the file's information + # on the new connection. + self.listener.file_props = fp + + self.connected += 1 return self.listener def send_success_reply(self, file_props, streamhost): @@ -100,58 +118,93 @@ class SocksQueue: file_props['streamhost-used'] is True: if 'proxyhosts' in file_props: for proxy in file_props['proxyhosts']: - if proxy == streamhost: - self.on_success(streamhost) - return 2 + if proxy['host'] == streamhost['host']: + self.on_success[file_props['sid']](proxy) + return 1 return 0 if 'streamhosts' in file_props: for host in file_props['streamhosts']: if streamhost['state'] == 1: return 0 streamhost['state'] = 1 - self.on_success(streamhost) + self.on_success[file_props['sid']](streamhost) return 1 return 0 - def connect_to_hosts(self, account, sid, on_success=None, on_failure=None): - self.on_success = on_success - self.on_failure = on_failure + def connect_to_hosts(self, account, sid, on_success=None, on_failure=None, + fingerprint=None, receiving=True): + self.on_success[sid] = on_success + self.on_failure[sid] = on_failure file_props = self.files_props[account][sid] file_props['failure_cb'] = on_failure + if not file_props['streamhosts']: + on_failure(file_props['sid']) + # add streamhosts to the queue for streamhost in file_props['streamhosts']: - receiver = Socks5Receiver(self.idlequeue, streamhost, sid, file_props) - self.add_receiver(account, receiver) - streamhost['idx'] = receiver.queue_idx + if 'type' in streamhost and streamhost['type'] == 'proxy': + fp = None + else: + fp = fingerprint + if receiving: + self.type = 'receiver' + socks5obj = Socks5ReceiverClient(self.idlequeue, streamhost, sid, + file_props, fingerprint=fp) + self.add_sockobj(account, socks5obj) + else: + if 'sha_str' in file_props: + idx = file_props['sha_str'] + else: + idx = self.idx + self.idx = self.idx + 1 + self.type = 'sender' + if 'type' in streamhost and streamhost['type'] == 'proxy': + file_props['is_a_proxy'] = True + file_props['proxy_sender'] = streamhost['target'] + file_props['proxy_receiver'] = streamhost['initiator'] + socks5obj = Socks5SenderClient(self.idlequeue, idx, + self, _sock=None,host=str(streamhost['host']), + port=int(streamhost['port']),fingerprint=fp, + connected=False, file_props=file_props) + socks5obj.streamhost = streamhost + self.add_sockobj(account, socks5obj, type='sender') + + streamhost['idx'] = socks5obj.queue_idx def _socket_connected(self, streamhost, file_props): """ Called when there is a host connected to one of the senders's - streamhosts. Stop othere attempts for connections + streamhosts. Stop other attempts for connections """ for host in file_props['streamhosts']: if host != streamhost and 'idx' in host: if host['state'] == 1: # remove current - self.remove_receiver(streamhost['idx']) + if self.type == 'sender': + self.remove_sender(streamhost['idx'], False) + else: + self.remove_receiver(streamhost['idx']) return # set state -2, meaning that this streamhost is stopped, # but it may be connectected later if host['state'] >= 0: - self.remove_receiver(host['idx']) + if self.type == 'sender': + self.remove_sender(host['idx'], False) + else: + self.remove_receiver(host['idx']) host['idx'] = -1 host['state'] = -2 - def reconnect_receiver(self, receiver, streamhost): + def reconnect_client(self, client, streamhost): """ Check the state of all streamhosts and if all has failed, then emit connection failure cb. If there are some which are still not connected try to establish connection to one of them """ - self.idlequeue.remove_timeout(receiver.fd) - self.idlequeue.unplug_idle(receiver.fd) - file_props = receiver.file_props + self.idlequeue.remove_timeout(client.fd) + self.idlequeue.unplug_idle(client.fd) + file_props = client.file_props streamhost['state'] = -1 # boolean, indicates that there are hosts, which are not tested yet unused_hosts = False @@ -165,20 +218,21 @@ class SocksQueue: for host in file_props['streamhosts']: if host['state'] == -2: host['state'] = 0 - receiver = Socks5Receiver(self.idlequeue, host, host['sid'], - file_props) - self.add_receiver(receiver.account, receiver) - host['idx'] = receiver.queue_idx + # FIXME: make the sender reconnect also + client = Socks5ReceiverClient(self.idlequeue, host, host['sid'], + file_props) + self.add_sockobj(client.account, client) + host['idx'] = client.queue_idx # we still have chances to connect return if 'received-len' not in file_props or file_props['received-len'] == 0: # there are no other streamhosts and transfer hasn't started - self._connection_refused(streamhost, file_props, receiver.queue_idx) + self._connection_refused(streamhost, file_props, client.queue_idx) else: # transfer stopped, it is most likely stopped from sender - receiver.disconnect() + client.disconnect() file_props['error'] = -1 - self.process_result(-1, receiver) + self.process_result(-1, client) def _connection_refused(self, streamhost, file_props, idx): """ @@ -187,42 +241,45 @@ class SocksQueue: if file_props is None: return streamhost['state'] = -1 + # FIXME: should only the receiver be remove? what if we are sending? self.remove_receiver(idx, False) if 'streamhosts' in file_props: for host in file_props['streamhosts']: if host['state'] != -1: return + self.readers = {} # failure_cb exists - this means that it has never been called if 'failure_cb' in file_props and file_props['failure_cb']: - file_props['failure_cb'](streamhost['initiator'], streamhost['id'], - file_props['sid'], code = 404) + file_props['failure_cb'](file_props['sid']) del(file_props['failure_cb']) - def add_receiver(self, account, sock5_receiver): + def add_sockobj(self, account, sockobj, type='receiver'): """ - Add new file request + Add new file a sockobj type receiver or sender, and use it to connect + to server """ - self.readers[self.idx] = sock5_receiver - sock5_receiver.queue_idx = self.idx - sock5_receiver.queue = self - sock5_receiver.account = account + if type == 'receiver': + self._add(sockobj, self.readers, sockobj.file_props, self.idx) + else: + self._add(sockobj, self.senders, sockobj.file_props, self.idx) + sockobj.queue_idx = self.idx + sockobj.queue = self + sockobj.account = account self.idx += 1 - result = sock5_receiver.connect() + result = sockobj.connect() self.connected += 1 if result is not None: - result = sock5_receiver.main() - self.process_result(result, sock5_receiver) + result = sockobj.main() + self.process_result(result, sockobj) return 1 return None - def get_file_from_sender(self, file_props, account): - if file_props is None: - return - if 'hash' in file_props and file_props['hash'] in self.senders: - sender = self.senders[file_props['hash']] - sender.account = account - result = self.get_file_contents(0) - self.process_result(result, sender) + def _add(self, sockobj, sockobjects, fp, hash): + ''' + Adds the sockobj to the current list of sockobjects + ''' + keys = (fp['sid'], fp['name'], hash) + sockobjects[keys] = sockobj def result_sha(self, sha_str, idx): if sha_str in self.sha_handlers: @@ -230,45 +287,52 @@ class SocksQueue: props[0](props[1], idx) def activate_proxy(self, idx): - if idx not in self.readers: + if not self.isHashInSockObjs(self.readers, idx): return - reader = self.readers[idx] - if reader.file_props['type'] != 's': - return - if reader.state != 5: - return - reader.state = 6 - if reader.connected: - reader.file_props['error'] = 0 - reader.file_props['disconnect_cb'] = reader.disconnect - reader.file_props['started'] = True - reader.file_props['completed'] = False - reader.file_props['paused'] = False - reader.file_props['stalled'] = False - reader.file_props['elapsed-time'] = 0 - reader.file_props['last-time'] = self.idlequeue.current_time() - reader.file_props['received-len'] = 0 - reader.pauses = 0 - # start sending file to proxy - self.idlequeue.set_read_timeout(reader.fd, STALLED_TIMEOUT) - self.idlequeue.plug_idle(reader, True, False) - result = reader.write_next() - self.process_result(result, reader) + for key in self.readers.keys(): + if idx in key: + reader = self.readers[key] + if reader.file_props['type'] != 's': + return + if reader.state != 5: + return + reader.state = 6 + if reader.connected: + reader.file_props['error'] = 0 + reader.file_props['disconnect_cb'] = reader.disconnect + reader.file_props['started'] = True + reader.file_props['completed'] = False + reader.file_props['paused'] = False + reader.file_props['stalled'] = False + reader.file_props['elapsed-time'] = 0 + reader.file_props['last-time'] = self.idlequeue.current_time() + reader.file_props['received-len'] = 0 + reader.pauses = 0 + # start sending file to proxy + self.idlequeue.set_read_timeout(reader.fd, STALLED_TIMEOUT) + self.idlequeue.plug_idle(reader, True, False) + result = reader.write_next() + self.process_result(result, reader) - def send_file(self, file_props, account): - if 'hash' in file_props and file_props['hash'] in self.senders: - sender = self.senders[file_props['hash']] - file_props['streamhost-used'] = True - sender.account = account - if file_props['type'] == 's': + def send_file(self, file_props, account, mode): + for key in self.senders.keys(): + if self.senders == {}: + # Python acts very weird with this. When there is no keys + # in the dictionary It says that it has a key. + # Maybe it is my machine. Without this there is a KeyError + # traceback. + return + if file_props['name'] in key and file_props['sid'] in key \ + and self.senders[key].mode == mode: + + log.info("socks5: sending file") + sender = self.senders[key] + file_props['streamhost-used'] = True + sender.account = account + sender.file_props = file_props result = sender.send_file() self.process_result(result, sender) - else: - file_props['elapsed-time'] = 0 - file_props['last-time'] = self.idlequeue.current_time() - file_props['received-len'] = 0 - sender.file_props = file_props def add_file_props(self, account, file_props): """ @@ -286,6 +350,10 @@ class SocksQueue: if account in self.files_props: fl_props = self.files_props[account] if sid in fl_props: + if sid in self.on_success: + del self.on_success[sid] + if sid in self.on_failure: + del self.on_failure[sid] del(fl_props[sid]) if len(self.files_props) == 0: @@ -301,13 +369,47 @@ class SocksQueue: return fl_props[sid] return None - def on_connection_accepted(self, sock): + def isHashInSockObjs(self, sockobjs, hash): + ''' + It tells wether there is a particular hash in sockobjs or not + ''' + for key in sockobjs: + if hash in key: + return True + return False + + def on_connection_accepted(self, sock, listener): sock_hash = sock.__hash__() - if sock_hash not in self.senders: - self.senders[sock_hash] = Socks5Sender(self.idlequeue, sock_hash, self, - sock[0], sock[1][0], sock[1][1]) + if self.type == 'sender' and \ + not self.isHashInSockObjs(self.senders, sock_hash): + + sockobj = Socks5SenderServer(self.idlequeue, sock_hash, self, + sock[0], sock[1][0], sock[1][1], fingerprint='server', + file_props=listener.file_props) + self._add(sockobj, self.senders, listener.file_props, sock_hash) + # Start waiting for data + self.idlequeue.plug_idle(sockobj, False, True) self.connected += 1 + if self.type == 'receiver' and \ + not self.isHashInSockObjs(self.readers, sock_hash): + sh = {} + sh['host'] = sock[1][0] + sh['port'] = sock[1][1] + sh['initiator'] = None + sh['target'] = None + + sockobj = Socks5ReceiverServer(idlequeue=self.idlequeue, + streamhost=sh,sid=None, file_props=listener.file_props, + fingerprint='server') + + self._add(sockobj, self.readers, listener.file_props, sock_hash) + + sockobj.set_sock(sock[0]) + sockobj.queue = self + self.connected += 1 + + def process_result(self, result, actor): """ Take appropriate actions upon the result: @@ -325,37 +427,49 @@ class SocksQueue: elif self.progress_transfer_cb is not None: self.progress_transfer_cb(actor.account, actor.file_props) - def remove_receiver(self, idx, do_disconnect=True): + def remove_receiver(self, idx, do_disconnect=True, remove_all=False): """ Remove reciver from the list and decrease the number of active connections with 1 """ if idx != -1: - if idx in self.readers: - reader = self.readers[idx] - self.idlequeue.unplug_idle(reader.fd) - self.idlequeue.remove_timeout(reader.fd) - if do_disconnect: - reader.disconnect() - else: - if reader.streamhost is not None: - reader.streamhost['state'] = -1 - del(self.readers[idx]) - - def remove_sender(self, idx, do_disconnect=True): + for key in self.readers.keys(): + if idx in key: + reader = self.readers[key] + self.idlequeue.unplug_idle(reader.fd) + self.idlequeue.remove_timeout(reader.fd) + if do_disconnect: + reader.disconnect() + if not remove_all: + break + else: + if reader.streamhost is not None: + reader.streamhost['state'] = -1 + del(self.readers[key]) + if not remove_all: + break + + def remove_sender(self, idx, do_disconnect=True, remove_all=False): """ Remove sender from the list of senders and decrease the number of active connections with 1 """ if idx != -1: - if idx in self.senders: - if do_disconnect: - self.senders[idx].disconnect() - return - else: - del(self.senders[idx]) - if self.connected > 0: - self.connected -= 1 + for key in self.senders.keys(): + if idx in key: + sender = self.senders[key] + if do_disconnect: + sender.disconnect() + if not remove_all: + break + else: + self.idlequeue.unplug_idle(sender.fd) + self.idlequeue.remove_timeout(sender.fd) + del(self.senders[key]) + if self.connected > 0: + self.connected -= 1 + if not remove_all: + break if len(self.senders) == 0 and self.listener is not None: self.listener.disconnect() self.listener = None @@ -367,7 +481,7 @@ class Socks5: try: self.host = host self.ais = socket.getaddrinfo(host, port, socket.AF_UNSPEC, - socket.SOCK_STREAM) + socket.SOCK_STREAM) except socket.gaierror: self.ais = None self.idlequeue = idlequeue @@ -383,6 +497,98 @@ class Socks5: self.size = 0 self.remaining_buff = '' self.file = None + self.connected = False + self.type = '' + self.mode = '' + + + def _is_connected(self): + if self.state < 5: + return False + return True + + def connect(self): + """ + Create the socket and plug it to the idlequeue + """ + if self.ais is None: + return None + + for ai in self.ais: + try: + self._sock = socket.socket(*ai[:3]) + + if not self.fingerprint is None: + self._sock = OpenSSL.SSL.Connection( + jingle_xtls.get_context('client'), self._sock) + # this will not block the GUI + self._sock.setblocking(False) + self._server = ai[4] + break + except socket.error, e: + if not isinstance(e, basestring) and e[0] == EINPROGRESS: + break + # for all other errors, we try other addresses + continue + self.fd = self._sock.fileno() + self.state = 0 # about to be connected + self.idlequeue.plug_idle(self, True, False) + self.do_connect() + self.idlequeue.set_read_timeout(self.fd, CONNECT_TIMEOUT) + return None + + def do_connect(self): + try: + #self._sock.setblocking(True) + self._sock.connect(self._server) + self._sock.setblocking(False) + self._send=self._sock.send + self._recv=self._sock.recv + except Exception, ee: + errnum = ee[0] + self.connect_timeout += 1 + if errnum == 111 or self.connect_timeout > 1000: + self.queue._connection_refused(self.streamhost, self.file_props, + self.queue_idx) + self.connected = False + return None + # win32 needs this + elif errnum not in (10056, EISCONN) or self.state != 0: + return None + else: # socket is already connected + self._sock.setblocking(False) + self._send=self._sock.send + self._recv=self._sock.recv + self.buff = '' + self.connected = True + self.file_props['connected'] = True + self.file_props['disconnect_cb'] = self.disconnect + self.file_props['paused'] = False + self.state = 1 # connected + + # stop all others connections to sender's streamhosts + self.queue._socket_connected(self.streamhost, self.file_props) + self.idlequeue.plug_idle(self, True, False) + return 1 # we are connected + + def read_timeout(self): + self.idlequeue.remove_timeout(self.fd) + if self.state > 5: + # no activity for foo seconds + if self.file_props['stalled'] == False: + self.file_props['stalled'] = True + self.queue.process_result(-1, self) + if 'received-len' not in self.file_props: + self.file_props['received-len'] = 0 + if SEND_TIMEOUT > 0: + self.idlequeue.set_read_timeout(self.fd, SEND_TIMEOUT) + else: + # stop transfer, there is no error code for this + self.pollend() + + else: + if self.mode == 'client': + self.queue.reconnect_client(self, self.streamhost) def open_file_for_reading(self): if self.file is None: @@ -441,6 +647,10 @@ class Socks5: received = '' try: add = self._recv(64) + except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError, + OpenSSL.SSL.WantX509LookupError), e: + log.info('SSL rehandshake request : ' + repr(e)) + raise e except Exception: add = '' received += add @@ -454,7 +664,11 @@ class Socks5: """ try: self._send(raw_data) - except Exception: + except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError, + OpenSSL.SSL.WantX509LookupError), e: + log.info('SSL rehandshake request :' + repr(e)) + raise e + except Exception, e: self.disconnect() return len(raw_data) @@ -475,6 +689,10 @@ class Socks5: lenn = 0 try: lenn = self._send(buff) + except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError, + OpenSSL.SSL.WantX509LookupError), e: + log.info('SSL rehandshake request :' + repr(e)) + raise e except Exception, e: if e.args[0] not in (EINTR, ENOBUFS, EWOULDBLOCK): # peer stopped reading @@ -485,7 +703,7 @@ class Socks5: self.size += lenn current_time = self.idlequeue.current_time() self.file_props['elapsed-time'] += current_time - \ - self.file_props['last-time'] + self.file_props['last-time'] self.file_props['last-time'] = current_time self.file_props['received-len'] = self.size if self.size >= int(self.file_props['size']): @@ -511,6 +729,7 @@ class Socks5: """ Read file contents from socket and write them to file """ + if self.file_props is None or ('file-name' in self.file_props) is False: self.file_props['error'] = -2 return None @@ -526,7 +745,7 @@ class Socks5: lenn = len(self.remaining_buff) current_time = self.idlequeue.current_time() self.file_props['elapsed-time'] += current_time - \ - self.file_props['last-time'] + self.file_props['last-time'] self.file_props['last-time'] = current_time self.file_props['received-len'] += lenn self.remaining_buff = '' @@ -545,25 +764,29 @@ class Socks5: return 0 try: buff = self._recv(MAX_BUFF_LEN) + except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError, + OpenSSL.SSL.WantX509LookupError), e: + log.info('SSL rehandshake request :' + repr(e)) + raise e except Exception: buff = '' current_time = self.idlequeue.current_time() self.file_props['elapsed-time'] += current_time - \ - self.file_props['last-time'] + self.file_props['last-time'] self.file_props['last-time'] = current_time self.file_props['received-len'] += len(buff) if len(buff) == 0: # Transfer stopped somehow: # reset, paused or network error self.rem_fd(fd) - self.disconnect(False) + self.disconnect() self.file_props['error'] = -1 return 0 try: fd.write(buff) except IOError, e: self.rem_fd(fd) - self.disconnect(False) + self.disconnect() self.file_props['error'] = -6 # file system error return 0 if self.file_props['received-len'] >= int(self.file_props['size']): @@ -591,6 +814,13 @@ class Socks5: self.close_file() self.idlequeue.remove_timeout(self.fd) self.idlequeue.unplug_idle(self.fd) + if self.mode == 'server': + try: + self.queue.listener.connections.remove(self._sock) + except ValueError: + pass # Not in list + if self.queue.listener.connections == []: + self.queue.listener.disconnect() try: self._sock.shutdown(socket.SHUT_RDWR) self._sock.close() @@ -628,10 +858,12 @@ class Socks5: return struct.pack('!BB', 0x05, 0x00) def _get_connect_buff(self): - ''' Connect request by domain name ''' + """ + Connect request by domain name + """ buff = struct.pack('!BBBBB%dsBB' % len(self.host), - 0x05, 0x01, 0x00, 0x03, len(self.host), self.host, - self.port >> 8, self.port & 0xff) + 0x05, 0x01, 0x00, 0x03, len(self.host), self.host, self.port >> 8, + self.port & 0xff) return buff def _get_request_buff(self, msg, command = 0x01): @@ -670,7 +902,12 @@ class Socks5: """ Connect response: version, auth method """ - buff = self._recv() + try: + buff = self._recv() + except (SSL.WantReadError, SSL.WantWriteError, + SSL.WantX509LookupError), e: + log.info("SSL rehandshake request : " + repr(e)) + raise e try: version, method = struct.unpack('!BB', buff) except Exception: @@ -690,26 +927,78 @@ class Socks5: """ Get sha of sid + Initiator jid + Target jid """ + if 'is_a_proxy' in self.file_props: del(self.file_props['is_a_proxy']) return hashlib.sha1('%s%s%s' % (self.sid, - self.file_props['proxy_sender'], - self.file_props['proxy_receiver'])).hexdigest() + self.file_props['proxy_sender'], + self.file_props['proxy_receiver'])).hexdigest() return hashlib.sha1('%s%s%s' % (self.sid, self.initiator, self.target)).\ hexdigest() + -class Socks5Sender(Socks5, IdleObject): +class Socks5Sender(IdleObject): """ Class for sending file to socket over socks5 """ def __init__(self, idlequeue, sock_hash, parent, _sock, host=None, - port=None): + port=None, fingerprint = None, connected=True, file_props={}): + + self.fingerprint = fingerprint self.queue_idx = sock_hash self.queue = parent - Socks5.__init__(self, idlequeue, host, port, None, None, None) + self.file_props = file_props + self.proxy = False + + self._sock = _sock - self._sock.setblocking(False) + + + if _sock is not None: + if self.fingerprint is not None: + self._sock = OpenSSL.SSL.Connection( + jingle_xtls.get_context('server'), _sock) + else: + self._sock.setblocking(False) + + self.fd = _sock.fileno() + self._recv = _sock.recv + self._send = _sock.send + self.connected = connected + self.state = 1 # waiting for first bytes + self.connect_timeout = 0 + + self.file_props['error'] = 0 + self.file_props['disconnect_cb'] = self.disconnect + self.file_props['started'] = True + self.file_props['completed'] = False + self.file_props['paused'] = False + self.file_props['continue_cb'] = self.continue_paused_transfer + self.file_props['stalled'] = False + self.file_props['connected'] = True + self.file_props['elapsed-time'] = 0 + self.file_props['last-time'] = self.idlequeue.current_time() + self.file_props['received-len'] = 0 + self.type = 'sender' + + def start_transfer(self): + """ + Send the file + """ + return self.write_next() + + + def set_connection_sock(self, _sock): + + self._sock = _sock + + if self.fingerprint is not None: + self._sock = OpenSSL.SSL.Connection( + jingle_xtls.get_context('client'), self._sock) + else: + self._sock.setblocking(False) + self.fd = _sock.fileno() self._recv = _sock.recv self._send = _sock.send @@ -719,75 +1008,59 @@ class Socks5Sender(Socks5, IdleObject): # start waiting for data self.idlequeue.plug_idle(self, False, True) - def read_timeout(self): - self.idlequeue.remove_timeout(self.fd) - if self.state > 5: - # no activity for foo seconds - if self.file_props['stalled'] == False: - self.file_props['stalled'] = True - self.queue.process_result(-1, self) - if SEND_TIMEOUT > 0: - self.idlequeue.set_read_timeout(self.fd, SEND_TIMEOUT) - else: - # stop transfer, there is no error code for this - self.pollend() - - def pollout(self): - if not self.connected: - self.disconnect() - return - self.idlequeue.remove_timeout(self.fd) - if self.state == 2: # send reply with desired auth type - self.send_raw(self._get_auth_response()) - elif self.state == 4: # send positive response to the 'connect' - self.send_raw(self._get_request_buff(self.sha_msg, 0x00)) - elif self.state == 7: - if self.file_props['paused']: - self.file_props['continue_cb'] = self.continue_paused_transfer - self.idlequeue.plug_idle(self, False, False) - return - result = self.write_next() - self.queue.process_result(result, self) - if result is None or result <= 0: - self.disconnect() - return - self.idlequeue.set_read_timeout(self.fd, STALLED_TIMEOUT) - elif self.state == 8: - self.disconnect() - return - else: - self.disconnect() - if self.state < 5: - self.state += 1 - # unplug and plug this time for reading - self.idlequeue.plug_idle(self, False, True) - - def pollend(self): - self.state = 8 # end connection - self.disconnect() - self.file_props['error'] = -1 - self.queue.process_result(-1, self) - - def pollin(self): - if self.connected: - if self.state < 5: - result = self.main() - if self.state == 4: - self.queue.result_sha(self.sha_msg, self.queue_idx) - if result == -1: - self.disconnect() - - elif self.state == 5: - if self.file_props is not None and self.file_props['type'] == 'r': - result = self.get_file_contents(0) - self.queue.process_result(result, self) - else: - self.disconnect() - def send_file(self): """ Start sending the file over verified connection """ + + self.pauses = 0 + self.state = 7 + # plug for writing + self.idlequeue.plug_idle(self, True, False) + return self.write_next() # initial for nl byte + + def disconnect(self, cb=True): + """ + Close the socket + """ + # close connection and remove us from the queue + Socks5.disconnect(self) + if self.file_props is not None: + self.file_props['connected'] = False + self.file_props['disconnect_cb'] = None + if self.queue is not None: + self.queue.remove_sender(self.queue_idx, False) + +class Socks5Receiver(IdleObject): + + def __init__(self, idlequeue, streamhost, sid, file_props = None, + fingerprint=None): + """ + fingerprint: fingerprint of certificates we shall use, set to None if + TLS connection not desired + """ + self.queue_idx = -1 + self.streamhost = streamhost + self.queue = None + self.fingerprint = fingerprint + self.connect_timeout = 0 + self.connected = False + self.pauses = 0 + self.file_props = file_props + self.file_props['disconnect_cb'] = self.disconnect + self.file_props['error'] = 0 + self.file_props['started'] = True + self.file_props['completed'] = False + self.file_props['paused'] = False + self.file_props['continue_cb'] = self.continue_paused_transfer + self.file_props['stalled'] = False + self.file_props['received-len'] = 0 + + + def receive_file(self): + """ + Start receiving the file over verified connection + """ if self.file_props['started']: return self.file_props['error'] = 0 @@ -803,9 +1076,44 @@ class Socks5Sender(Socks5, IdleObject): self.file_props['received-len'] = 0 self.pauses = 0 self.state = 7 - # plug for writing - self.idlequeue.plug_idle(self, True, False) - return self.write_next() # initial for nl byte + # plug for reading + self.idlequeue.plug_idle(self, False, True) + return self.get_file_contents(0) # initial for nl byte + + def start_transfer(self): + """ + Receive the file + """ + return self.get_file_contents(0) + + def set_sock(self, _sock): + self._sock = _sock + self._sock.setblocking(False) + self.fd = _sock.fileno() + self._recv = _sock.recv + self._send = _sock.send + self.connected = True + self.state = 1 # waiting for first bytes + # start waiting for data + self.idlequeue.plug_idle(self, False, True) + + def disconnect(self, cb=True): + """ + Close the socket. Remove self from queue if cb is True + """ + # close connection + Socks5.disconnect(self) + if cb is True: + self.file_props['disconnect_cb'] = None + if self.queue is not None: + self.queue.remove_receiver(self.queue_idx, False) + +class Socks5Server(Socks5): + def __init__(self, idlequeue, host, port, initiator, target, sid): + + Socks5.__init__(self, idlequeue, host, port, initiator, target, sid) + + self.mode = 'server' def main(self): """ @@ -828,258 +1136,88 @@ class Socks5Sender(Socks5, IdleObject): self.idlequeue.plug_idle(self, True, False) return None - def disconnect(self, cb=True): - """ - Close the socket - """ - # close connection and remove us from the queue - Socks5.disconnect(self) - if self.file_props is not None: - self.file_props['connected'] = False - self.file_props['disconnect_cb'] = None - if self.queue is not None: - self.queue.remove_sender(self.queue_idx, False) - -class Socks5Listener(IdleObject): - def __init__(self, idlequeue, port): - """ - Handle all incomming connections on (0.0.0.0, port) - - This class implements IdleObject, but we will expect - only pollin events though - """ - self.port = port - self.ais = socket.getaddrinfo(None, port, socket.AF_UNSPEC, - socket.SOCK_STREAM, socket.SOL_TCP, socket.AI_PASSIVE) - self.ais.sort(reverse=True) # Try IPv6 first - self.queue_idx = -1 - self.idlequeue = idlequeue - self.queue = None - self.started = False - self._sock = None - self.fd = -1 - - def bind(self): - for ai in self.ais: - # try the different possibilities (ipv6, ipv4, etc.) - try: - self._serv = socket.socket(*ai[:3]) - except socket.error, e: - if e.args[0] == EAFNOSUPPORT: - self.ai = None - continue - raise - self._serv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self._serv.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) - self._serv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - # Under windows Vista, we need that to listen on ipv6 AND ipv4 - # Doesn't work under windows XP - if os.name == 'nt': - ver = os.sys.getwindowsversion() - if (ver[3], ver[0], ver[1]) == (2, 6, 0): - # 27 is socket.IPV6_V6ONLY under windows, but not defined ... - self._serv.setsockopt(socket.IPPROTO_IPV6, 27, 1) - # will fail when port as busy, or we don't have rights to bind - try: - self._serv.bind(ai[4]) - self.ai = ai - break - except Exception: - self.ai = None - continue - if not self.ai: - # unable to bind, show error dialog - return None - self._serv.listen(socket.SOMAXCONN) - self._serv.setblocking(False) - self.fd = self._serv.fileno() - self.idlequeue.plug_idle(self, False, True) - self.started = True - - def pollend(self): - """ - Called when we stop listening on (host, port) - """ - self.disconnect() - - def pollin(self): - """ - Accept a new incomming connection and notify queue - """ - sock = self.accept_conn() - self.queue.on_connection_accepted(sock) - - def disconnect(self): - """ - Free all resources, we are not listening anymore - """ - self.idlequeue.remove_timeout(self.fd) - self.idlequeue.unplug_idle(self.fd) - self.fd = -1 - self.state = -1 - self.started = False - try: - self._serv.close() - except Exception: - pass - - def accept_conn(self): - """ - Accept a new incomming connection - """ - _sock = self._serv.accept() - _sock[0].setblocking(False) - return _sock - -class Socks5Receiver(Socks5, IdleObject): - def __init__(self, idlequeue, streamhost, sid, file_props = None): - self.queue_idx = -1 - self.streamhost = streamhost - self.queue = None - self.file_props = file_props - self.connect_timeout = 0 - self.connected = False - self.pauses = 0 - if not self.file_props: - self.file_props = {} - self.file_props['disconnect_cb'] = self.disconnect - self.file_props['error'] = 0 - self.file_props['started'] = True - self.file_props['completed'] = False - self.file_props['paused'] = False - self.file_props['continue_cb'] = self.continue_paused_transfer - self.file_props['stalled'] = False - Socks5.__init__(self, idlequeue, streamhost['host'], - int(streamhost['port']), streamhost['initiator'], streamhost['target'], - sid) - - def read_timeout(self): - self.idlequeue.remove_timeout(self.fd) - if self.state > 5: - # no activity for foo seconds - if self.file_props['stalled'] == False: - self.file_props['stalled'] = True - if 'received-len' not in self.file_props: - self.file_props['received-len'] = 0 - self.queue.process_result(-1, self) - if READ_TIMEOUT > 0: - self.idlequeue.set_read_timeout(self.fd, READ_TIMEOUT) - else: - # stop transfer, there is no error code for this - self.pollend() - else: - self.queue.reconnect_receiver(self, self.streamhost) - - def connect(self): - """ - Create the socket and plug it to the idlequeue - """ - if self.ais is None: - return None - - for ai in self.ais: - try: - self._sock = socket.socket(*ai[:3]) - # this will not block the GUI - self._sock.setblocking(False) - self._server = ai[4] - break - except socket.error, e: - if not isinstance(e, basestring) and e[0] == EINPROGRESS: - break - # for all other errors, we try other addresses - continue - self.fd = self._sock.fileno() - self.state = 0 # about to be connected - self.idlequeue.plug_idle(self, True, False) - self.do_connect() - self.idlequeue.set_read_timeout(self.fd, CONNECT_TIMEOUT) - return None - - def _is_connected(self): - if self.state < 5: - return False - return True - - def pollout(self): - self.idlequeue.remove_timeout(self.fd) - if self.state == 0: - self.do_connect() - return - elif self.state == 1: # send initially: version and auth types - self.send_raw(self._get_auth_buff()) - elif self.state == 3: # send 'connect' request - self.send_raw(self._get_request_buff(self._get_sha1_auth())) - elif self.file_props['type'] != 'r': - if self.file_props['paused']: - self.idlequeue.plug_idle(self, False, False) - return - result = self.write_next() - self.queue.process_result(result, self) - return - self.state += 1 - # unplug and plug for reading - self.idlequeue.plug_idle(self, False, True) - self.idlequeue.set_read_timeout(self.fd, CONNECT_TIMEOUT) - - def pollend(self): - if self.state >= 5: - # error during transfer - self.disconnect() - self.file_props['error'] = -1 - self.queue.process_result(-1, self) - else: - self.queue.reconnect_receiver(self, self.streamhost) def pollin(self): self.idlequeue.remove_timeout(self.fd) if self.connected: - if self.file_props['paused']: - self.idlequeue.plug_idle(self, False, False) - return - if self.state < 5: - self.idlequeue.set_read_timeout(self.fd, CONNECT_TIMEOUT) - result = self.main(0) - self.queue.process_result(result, self) - elif self.state == 5: # wait for proxy reply - pass - elif self.file_props['type'] == 'r': - self.idlequeue.set_read_timeout(self.fd, STALLED_TIMEOUT) - result = self.get_file_contents(0) - self.queue.process_result(result, self) + try: + if self.state < 5: + result = self.main() + if self.state == 4: + self.queue.result_sha(self.sha_msg, self.queue_idx) + if result == -1: + self.disconnect() + + elif self.state == 5: + self.state = 7 + if self.type == 'sender': + # We wait for the end of the negotiation to + # send the file + self.idlequeue.plug_idle(self, False, False) + else: + # We plug for reading + self.idlequeue.plug_idle(self, False, True) + return + + elif self.state == 7: + if self.file_props['paused']: + self.file_props['continue_cb'] = self.continue_paused_transfer + self.idlequeue.plug_idle(self, False, False) + return + self.idlequeue.set_read_timeout(self.fd, STALLED_TIMEOUT) + result = self.start_transfer() # send + self.queue.process_result(result, self) + except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError, + OpenSSL.SSL.WantX509LookupError), e: + log.info('caught SSL exception, ignored') else: self.disconnect() - def do_connect(self): - try: - self._sock.connect(self._server) - self._sock.setblocking(False) - self._send=self._sock.send - self._recv=self._sock.recv - except Exception, ee: - errnum = ee[0] - self.connect_timeout += 1 - if errnum == 111 or self.connect_timeout > 1000: - self.queue._connection_refused(self.streamhost, - self.file_props, self.queue_idx) - return None - # win32 needs this - elif errnum not in (10056, EISCONN) or self.state != 0: - return None - else: # socket is already connected - self._sock.setblocking(False) - self._send=self._sock.send - self._recv=self._sock.recv - self.buff = '' - self.connected = True - self.file_props['connected'] = True - self.file_props['disconnect_cb'] = self.disconnect - self.state = 1 # connected - # stop all others connections to sender's streamhosts - self.queue._socket_connected(self.streamhost, self.file_props) - self.idlequeue.plug_idle(self, True, False) - return 1 # we are connected + def pollend(self): + self.state = 8 # end connection + self.disconnect() + self.file_props['error'] = -1 + self.queue.process_result(-1, self) + + def pollout(self): + if not self.connected: + self.disconnect() + return + self.idlequeue.remove_timeout(self.fd) + if self.state == 2: # send reply with desired auth type + self.send_raw(self._get_auth_response()) + elif self.state == 4: # send positive response to the 'connect' + self.send_raw(self._get_request_buff(self.sha_msg, 0x00)) + elif self.state == 7: + if self.file_props['paused']: + self.file_props['continue_cb'] = self.continue_paused_transfer + self.idlequeue.plug_idle(self, False, False) + return + result = self.start_transfer() # send + self.queue.process_result(result, self) + if result is None or result <= 0: + self.disconnect() + return + self.idlequeue.set_read_timeout(self.fd, STALLED_TIMEOUT) + elif self.state == 8: + self.disconnect() + return + else: + self.disconnect() + if self.state < 5: + self.state += 1 + # unplug and plug this time for reading + self.idlequeue.plug_idle(self, False, True) + + +class Socks5Client(Socks5): + + def __init__(self, idlequeue, host, port, initiator, target, sid): + + Socks5.__init__(self, idlequeue, host, port, initiator, target, sid) + + self.mode = 'client' def main(self, timeout=0): """ @@ -1120,7 +1258,12 @@ class Socks5Receiver(Socks5, IdleObject): self.state = 5 # for senders: init file_props and send '\n' if self.queue.on_success: result = self.queue.send_success_reply(self.file_props, - self.streamhost) + self.streamhost) + if self.type == 'sender' and self.proxy: + self.queue.process_result( self.send_file() + , self) + return + if result == 0: self.state = 8 self.disconnect() @@ -1139,11 +1282,13 @@ class Socks5Receiver(Socks5, IdleObject): self.file_props['received-len'] = 0 self.pauses = 0 # start sending file contents to socket - self.idlequeue.set_read_timeout(self.fd, STALLED_TIMEOUT) - self.idlequeue.plug_idle(self, True, False) + #self.idlequeue.set_read_timeout(self.fd, STALLED_TIMEOUT) + #self.idlequeue.plug_idle(self, True, False) + self.idlequeue.plug_idle(self, False, False) else: # receiving file contents from socket self.idlequeue.plug_idle(self, False, True) + self.file_props['continue_cb'] = self.continue_paused_transfer # we have set up the connection, next - retrieve file self.state = 6 @@ -1152,13 +1297,224 @@ class Socks5Receiver(Socks5, IdleObject): self.state += 1 return None - def disconnect(self, cb=True): + + def pollin(self): + self.idlequeue.remove_timeout(self.fd) + if self.connected: + try: + if self.file_props['paused']: + self.idlequeue.plug_idle(self, False, False) + return + if self.state < 5: + self.idlequeue.set_read_timeout(self.fd, CONNECT_TIMEOUT) + result = self.main(0) + self.queue.process_result(result, self) + elif self.state == 5: # wait for proxy reply + pass + elif self.file_props['type'] == 'r': + self.idlequeue.set_read_timeout(self.fd, STALLED_TIMEOUT) + result = self.start_transfer() # receive + self.queue.process_result(result, self) + except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError, + OpenSSL.SSL.WantX509LookupError), e: + log.info('caught SSL exception, ignored') + return + else: + self.disconnect() + + def pollout(self): + self.idlequeue.remove_timeout(self.fd) + try: + if self.state == 0: + self.do_connect() + return + elif self.state == 1: # send initially: version and auth types + self.send_raw(self._get_auth_buff()) + elif self.state == 3: # send 'connect' request + self.send_raw(self._get_request_buff(self._get_sha1_auth())) + elif self.file_props['type'] != 'r': + if self.file_props['paused']: + self.idlequeue.plug_idle(self, False, False) + return + result = self.start_transfer() # send + self.queue.process_result(result, self) + return + except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError, + OpenSSL.SSL.WantX509LookupError), e: + log.info('caught SSL exception, ignored') + return + self.state += 1 + # unplug and plug for reading + self.idlequeue.plug_idle(self, False, True) + self.idlequeue.set_read_timeout(self.fd, CONNECT_TIMEOUT) + + def pollend(self): + if self.state >= 5: + # error during transfer + self.disconnect() + self.file_props['error'] = -1 + self.queue.process_result(-1, self) + else: + self.queue.reconnect_client(self, self.streamhost) + + + +class Socks5SenderClient(Socks5Client, Socks5Sender): + + def __init__(self, idlequeue, sock_hash, parent,_sock, host=None, + port=None, fingerprint = None, connected=True, file_props={}): + + Socks5Client.__init__(self, idlequeue, host, port, None, None, + file_props['sid']) + + Socks5Sender.__init__(self,idlequeue, sock_hash, parent,_sock, + host, port, fingerprint , connected, file_props) + + + + + +class Socks5SenderServer(Socks5Server, Socks5Sender): + + def __init__(self, idlequeue, sock_hash, parent,_sock, host=None, + port=None, fingerprint = None, connected=True, file_props={}): + + Socks5Server.__init__(self, idlequeue, host, port, None, None, + file_props['sid']) + + Socks5Sender.__init__(self,idlequeue, sock_hash, parent, _sock, + host, port, fingerprint , connected, file_props) + + + +class Socks5ReceiverClient(Socks5Client, Socks5Receiver): + + def __init__(self, idlequeue, streamhost, sid, file_props = None, + fingerprint=None): + Socks5Client.__init__(self, idlequeue, streamhost['host'], + int(streamhost['port']), streamhost['initiator'], + streamhost['target'], sid) + + Socks5Receiver.__init__(self, idlequeue, streamhost, sid, file_props, + fingerprint) + + + + +class Socks5ReceiverServer(Socks5Server, Socks5Receiver): + + def __init__(self, idlequeue, streamhost, sid, file_props = None, + fingerprint=None): + + Socks5Server.__init__(self, idlequeue, streamhost['host'], + int(streamhost['port']), streamhost['initiator'], + streamhost['target'], sid) + + Socks5Receiver.__init__(self, idlequeue, streamhost, sid, file_props, + fingerprint) + + + +class Socks5Listener(IdleObject): + def __init__(self, idlequeue, port, fp, fingerprint=None): """ - Close the socket. Remove self from queue if cb is True + Handle all incomming connections on (0.0.0.0, port) + + This class implements IdleObject, but we will expect + only pollin events though + + fingerprint: fingerprint of certificates we shall use, set to None if + TLS connection not desired """ - # close connection - Socks5.disconnect(self) - if cb is True: - self.file_props['disconnect_cb'] = None - if self.queue is not None: - self.queue.remove_receiver(self.queue_idx, False) + self.port = port + self.ais = socket.getaddrinfo(None, port, socket.AF_UNSPEC, + socket.SOCK_STREAM, socket.SOL_TCP, socket.AI_PASSIVE) + self.ais.sort(reverse=True) # Try IPv6 first + self.queue_idx = -1 + self.idlequeue = idlequeue + self.queue = None + self.started = False + self._sock = None + self.fd = -1 + self.fingerprint = fingerprint + self.file_props = fp + self.connections = [] + + def bind(self): + for ai in self.ais: + # try the different possibilities (ipv6, ipv4, etc.) + try: + self._serv = socket.socket(*ai[:3]) + if self.fingerprint is not None: + self._serv = OpenSSL.SSL.Connection( + jingle_xtls.get_context('server'), self._serv) + except socket.error, e: + if e.args[0] == EAFNOSUPPORT: + self.ai = None + continue + raise + self._serv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._serv.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + self._serv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + # Under windows Vista, we need that to listen on ipv6 AND ipv4 + # Doesn't work under windows XP + if os.name == 'nt': + ver = os.sys.getwindowsversion() + if (ver[3], ver[0], ver[1]) == (2, 6, 0): + # 27 is socket.IPV6_V6ONLY under windows, but not defined ... + self._serv.setsockopt(socket.IPPROTO_IPV6, 27, 1) + # will fail when port as busy, or we don't have rights to bind + try: + self._serv.bind(ai[4]) + f = ai[4] + self.ai = ai + break + except Exception: + self.ai = None + continue + if not self.ai: + # unable to bind, show error dialog + return None + self._serv.listen(socket.SOMAXCONN) + self._serv.setblocking(False) + self.fd = self._serv.fileno() + self.idlequeue.plug_idle(self, False, True) + self.started = True + + def pollend(self): + """ + Called when we stop listening on (host, port) + """ + self.disconnect() + + def pollin(self): + """ + Accept a new incomming connection and notify queue + """ + sock = self.accept_conn() + self.queue.on_connection_accepted(sock, self) + + def disconnect(self): + """ + Free all resources, we are not listening anymore + """ + self.idlequeue.remove_timeout(self.fd) + self.idlequeue.unplug_idle(self.fd) + self.fd = -1 + self.state = -1 + self.started = False + try: + self._serv.close() + except Exception: + pass + + def accept_conn(self): + """ + Accept a new incomming connection + """ + _sock = self._serv.accept() + _sock[0].setblocking(False) + self.connections.append(_sock[0]) + return _sock + + diff --git a/src/common/xmpp/protocol.py b/src/common/xmpp/protocol.py index 1c92d1f47..e0bd794a3 100644 --- a/src/common/xmpp/protocol.py +++ b/src/common/xmpp/protocol.py @@ -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) + diff --git a/src/dialogs.py b/src/dialogs.py index 42a33d830..d5456d663 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -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: diff --git a/src/filetransfers_window.py b/src/filetransfers_window.py index 9b50f8dbf..4537c5141 100644 --- a/src/filetransfers_window.py +++ b/src/filetransfers_window.py @@ -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: diff --git a/src/gui_interface.py b/src/gui_interface.py index 895898127..c00b3283b 100644 --- a/src/gui_interface.py +++ b/src/gui_interface.py @@ -34,7 +34,6 @@ ## You should have received a copy of the GNU General Public License ## along with Gajim. If not, see . ## - 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: diff --git a/src/gui_menu_builder.py b/src/gui_menu_builder.py index ded6e6dc1..9df948739 100644 --- a/src/gui_menu_builder.py +++ b/src/gui_menu_builder.py @@ -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) diff --git a/src/roster_window.py b/src/roster_window.py index 3bf2065c2..367ac3b52 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -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) diff --git a/test/unit/test_jingle.py b/test/unit/test_jingle.py new file mode 100644 index 000000000..94131ede7 --- /dev/null +++ b/test/unit/test_jingle.py @@ -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 = ''' + + + + + + + + + + + + + + + + + + + + + + ''' + + +transport_info = ''' + + + + + + + + + + +''' + +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("") + + 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() diff --git a/test/unit/test_socks5.py b/test/unit/test_socks5.py new file mode 100644 index 000000000..3d5d92943 --- /dev/null +++ b/test/unit/test_socks5.py @@ -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()