# -*- 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 hashlib import logging import os import threading from enum import IntEnum, unique import nbxmpp from common import gajim from common import configpaths from common import jingle_xtls from common.jingle_content import contents, JingleContent from common.jingle_transport import JingleTransportSocks5, TransportType from common import helpers from common.connection_handlers_events import FileRequestReceivedEvent from common.jingle_ftstates import ( StateInitialized, StateCandSent, StateCandReceived, StateTransfering, StateCandSentAndRecv, StateTransportReplace) log = logging.getLogger('gajim.c.jingle_ft') @unique class State(IntEnum): NOT_STARTED = 0 INITIALIZED = 1 # We send the candidates and we are waiting for a reply CAND_SENT = 2 # We received the candidates and we are waiting to reply CAND_RECEIVED = 3 # We have sent and received the candidates # This also includes any candidate-error received or sent CAND_SENT_AND_RECEIVED = 4 TRANSPORT_REPLACE = 5 # We are transfering the file 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.x509_fingerprint = None self.file_props = file_props self.weinitiate = self.session.weinitiate self.werequest = self.session.werequest if self.file_props is not None: if self.session.werequest: self.file_props.sender = self.session.peerjid self.file_props.receiver = self.session.ourjid else: self.file_props.sender = self.session.ourjid self.file_props.receiver = self.session.peerjid self.file_props.session_type = 'jingle' self.file_props.sid = session.sid self.file_props.transfered_size = [] self.file_props.transport_sid = self.transport.sid 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) self.session = session self.media = 'file' self.nominated_cand = {} if gajim.contacts.is_gc_contact(session.connection.name, session.peerjid): roomjid = session.peerjid.split('/')[0] dstaddr = hashlib.sha1(('%s%s%s' % (self.file_props.sid, session.ourjid, roomjid)) .encode('utf-8')).hexdigest() self.file_props.dstaddr = dstaddr 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) } if jingle_xtls.PYOPENSSL_PRESENT: cert_name = os.path.join(configpaths.gajimpaths['MY_CERT'], 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 __state_changed(self, nextstate, args=None): # Executes the next state action and sets the next state current_state = self.state st = self.states[nextstate] st.action(args) # state can have been changed during the action. Don't go back. if self.state == current_state: self.state = nextstate def __on_session_initiate(self, stanza, content, error, action): log.debug("Jingle FT request received") gajim.nec.push_incoming_event(FileRequestReceivedEvent(None, conn=self.session.connection, stanza=stanza, jingle_content=content, FT_content=self)) if self.session.request: # accept the request self.session.approve_content(self.media, self.name) self.session.accept_session() def __on_session_initiate_sent(self, stanza, content, error, action): pass def __send_hash(self): # Send hash in a session info checksum = nbxmpp.Node(tag='checksum', payload=[nbxmpp.Node(tag='file', payload=[self._compute_hash()])]) checksum.setNamespace(nbxmpp.NS_JINGLE_FILE_TRANSFER) self.session.__session_info(checksum) pjid = gajim.get_jid_without_resource(self.session.peerjid) file_info = {'name' : self.file_props.name, 'file-name' : self.file_props.file_name, 'hash' : self.file_props.hash_, 'size' : self.file_props.size, 'date' : self.file_props.date, 'peerjid' : pjid } self.session.connection.set_file_info(file_info) def _compute_hash(self): # Caculates the hash and returns a xep-300 hash stanza if self.file_props.algo is None: return try: file_ = open(self.file_props.file_name, 'rb') except IOError: # can't open file return h = nbxmpp.Hashes() hash_ = h.calculateHash(self.file_props.algo, file_) file_.close() # DEBUG #hash_ = '1294809248109223' if not hash_: # Hash alogrithm not supported return self.file_props.hash_ = hash_ h.addHash(hash_, self.file_props.algo) return h def on_cert_received(self): self.session.approve_session() self.session.approve_content('file', name=self.name) 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 else: fingerprint = security.getTag('fingerprint') if fingerprint: fingerprint = fingerprint.getData() self.x509_fingerprint = fingerprint if not jingle_xtls.check_cert(gajim.get_jid_without_resource( self.session.responder), fingerprint): id_ = jingle_xtls.send_cert_request(con, self.session.responder) jingle_xtls.key_exchange_pend(id_, self.continue_session_accept, [stanza]) raise nbxmpp.NodeProcessed self.continue_session_accept(stanza) def continue_session_accept(self, stanza): con = self.session.connection if self.state == State.TRANSPORT_REPLACE: # If we are requesting we don't have the file if self.session.werequest: raise nbxmpp.NodeProcessed # We send the file self.__state_changed(State.TRANSFERING) raise nbxmpp.NodeProcessed self.file_props.streamhosts = self.transport.remote_candidates # Calculate file hash in a new thread # if we haven't sent the hash already. if self.file_props.hash_ is None and self.file_props.algo and \ not self.werequest: self.hash_thread = threading.Thread(target=self.__send_hash) self.hash_thread.start() for host in self.file_props.streamhosts: host['initiator'] = self.session.initiator host['target'] = self.session.responder host['sid'] = self.file_props.sid 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) raise nbxmpp.NodeProcessed self.__state_changed(State.TRANSFERING) raise nbxmpp.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") cand_error = content.getTag('transport').getTag('candidate-error') cand_used = content.getTag('transport').getTag('candidate-used') if (cand_error or cand_used) and \ self.state >= State.CAND_SENT_AND_RECEIVED: raise nbxmpp.NodeProcessed if cand_error: if not gajim.socks5queue.listener.connections: gajim.socks5queue.listener.disconnect() 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 nbxmpp.NodeProcessed else: args = {'cand_error' : True} self.__state_changed(State.CAND_RECEIVED, args) return if cand_used: streamhost_cid = cand_used.getAttr('cid') streamhost_used = None for cand in self.transport.candidates: if cand['candidate_id'] == streamhost_cid: streamhost_used = cand break if streamhost_used is None or streamhost_used['type'] == 'proxy': if gajim.socks5queue.listener and \ not gajim.socks5queue.listener.connections: gajim.socks5queue.listener.disconnect() 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) self.__state_changed(State.TRANSFERING) raise nbxmpp.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 else: self._listen_host() 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.__send_candidate(args) def _on_connect_error(self, sid): log.info('connect error, sid=' + sid) args = {'candError' : True, 'sendCand' : True} self.__send_candidate(args) def __send_candidate(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 _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' listener = gajim.socks5queue.start_listener(port, sha_str, self._store_socks5_sid, self.file_props, fingerprint=fingerprint, typ='sender' if self.weinitiate else 'receiver') if not listener: # send error message, notify the user return def is_our_candidate_used(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 not self.nominated_cand['peer-cand']: return True if not self.nominated_cand['our-cand']: 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 return self.weinitiate def start_ibb_transfer(self): if self.file_props.type_ == 's': self.__state_changed(State.TRANSFERING) def get_content(desc): return JingleFileTransfer contents[nbxmpp.NS_JINGLE_FILE_TRANSFER] = get_content