Skip to content
Snippets Groups Projects
jingle_ft.py 13.2 KiB
Newer Older
# -*- coding:utf-8 -*-
## This file is part of Gajim.
##
## Gajim is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
## by the Free Software Foundation; version 3 only.
##
## Gajim is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
##


"""
Handles  Jingle File Transfer (XEP 0234)
"""

import gajim
import xmpp
from jingle_content import contents, JingleContent
from jingle_transport import *
zimio's avatar
zimio committed
from common.socks5 import Socks5ReceiverClient, Socks5SenderClient
from common.connection_handlers_events import FileRequestReceivedEvent
zimio's avatar
zimio committed
import threading
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
zimio's avatar
zimio committed
STATE_CAND_SENT = 2
# We received the candidates and we are waiting to reply
zimio's avatar
zimio committed
STATE_CAND_RECEIVED = 3
# We have sent and received the candidates
# This also includes any candidate-error received or sent
zimio's avatar
zimio committed
STATE_CAND_SENT_AND_RECEIVED = 4
STATE_TRANSPORT_REPLACE = 5
# We are transfering the file
zimio's avatar
zimio committed
STATE_TRANSFERING = 6

class JingleFileTransfer(JingleContent):
Yann Leboulanger's avatar
Yann Leboulanger committed
    def __init__(self, session, transport=None, file_props=None,
    use_security=False):
        JingleContent.__init__(self, session, transport)
Yann Leboulanger's avatar
Yann Leboulanger committed

        log.info("transport value: %s" % transport)
Yann Leboulanger's avatar
Yann Leboulanger committed

        # events we might be interested in
        self.callbacks['session-initiate'] += [self.__on_session_initiate]
Yann Leboulanger's avatar
Yann Leboulanger committed
        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]
Yann Leboulanger's avatar
Yann Leboulanger committed
        self.callbacks['session-terminate'] += [self.__on_session_terminate]
zimio's avatar
zimio committed
        self.callbacks['session-info'] += [self.__on_session_info]
        self.callbacks['transport-accept'] += [self.__on_transport_accept]
Yann Leboulanger's avatar
Yann Leboulanger committed
        self.callbacks['transport-replace'] += [self.__on_transport_replace]
        self.callbacks['session-accept-sent'] += [self.__transport_setup]
Yann Leboulanger's avatar
Yann Leboulanger committed
        # 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]
Yann Leboulanger's avatar
Yann Leboulanger committed

        self.file_props = file_props
            self.weinitiate = False
        else:
            self.weinitiate = True
zimio's avatar
zimio committed
            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 = []
Yann Leboulanger's avatar
Yann Leboulanger committed
        log.info("FT request: %s" % file_props)
            self.transport = JingleTransportSocks5()
zimio's avatar
zimio committed
        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:
zimio's avatar
zimio committed
            self.file_props.sid = self.transport.sid

        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)
Yann Leboulanger's avatar
Yann Leboulanger committed
        }

    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
Yann Leboulanger's avatar
Yann Leboulanger committed

    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))
Yann Leboulanger's avatar
Yann Leboulanger committed
        self._listen_host()
        # Delete this after file_props refactoring this shouldn't be necesary
zimio's avatar
zimio committed
        self.session.file_hash = self.file_props.hash_
        self.session.hash_algo = self.file_props.algo
zimio's avatar
zimio committed
    def __on_session_initiate_sent(self, stanza, content, error, action):
        # Calculate file_hash in a new thread
        # if we haven't sent the hash already.
zimio's avatar
zimio committed
        if self.file_props.hash_ is None:
            self.hashThread = threading.Thread(target=self.__send_hash)
            self.hashThread.start()
    def __send_hash(self):
        # Send hash in a session info
Yann Leboulanger's avatar
Yann Leboulanger committed
        checksum = xmpp.Node(tag='checksum', payload=[xmpp.Node(tag='file',
            payload=[self._calcHash()])])
        checksum.setNamespace(xmpp.NS_JINGLE_FILE_TRANSFER)
        self.session.__session_info(checksum )

    def _calcHash(self):
        # Caculates the hash and returns a xep-300 hash stanza
        if self.session.hash_algo == None:
zimio's avatar
zimio committed
            return
        try:
zimio's avatar
zimio committed
            file_ = open(self.file_props.file_name, 'r')
zimio's avatar
zimio committed
        except:
            # can't open file
zimio's avatar
zimio committed
            return
        h = xmpp.Hashes()
        hash_ = h.calculateHash(self.session.hash_algo, file_)
        # DEBUG
        #hash_ = '1294809248109223'
        if not hash_:
            # Hash alogrithm not supported
            return
zimio's avatar
zimio committed
        self.file_props.hash_ = hash_
zimio's avatar
zimio committed
        h.addHash(hash_, self.session.hash_algo)
    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
zimio's avatar
zimio committed
        if self.state == STATE_TRANSPORT_REPLACE:
            # We ack the session accept
            response = stanza.buildReply('result')
Yann Leboulanger's avatar
Yann Leboulanger committed
            response.delChild(response.getQuery())
zimio's avatar
zimio committed
            con.connection.send(response)
            # We send the file
            self.__state_changed(STATE_TRANSFERING)
zimio's avatar
zimio committed
            raise xmpp.NodeProcessed
zimio's avatar
zimio committed
        self.file_props.streamhosts = self.transport.remote_candidates
        for host in self.file_props.streamhosts:
Yann Leboulanger's avatar
Yann Leboulanger committed
            host['initiator'] = self.session.initiator
            host['target'] = self.session.responder
zimio's avatar
zimio committed
            host['sid'] = self.file_props.sid
        response = stanza.buildReply('result')
Yann Leboulanger's avatar
Yann Leboulanger committed
        response.delChild(response.getQuery())
        con.connection.send(response)
        fingerprint = None
        if self.use_security:
            fingerprint = 'client'
Yann Leboulanger's avatar
Yann Leboulanger committed
        if self.transport.type_ == TransportType.SOCKS5:
            gajim.socks5queue.connect_to_hosts(self.session.connection.name,
zimio's avatar
zimio committed
                self.file_props.sid, self.on_connect,
Yann Leboulanger's avatar
Yann Leboulanger committed
                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")

zimio's avatar
zimio committed
    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")
Yann Leboulanger's avatar
Yann Leboulanger committed

    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")
Yann Leboulanger's avatar
Yann Leboulanger committed

zimio's avatar
zimio committed
        if content.getTag('transport').getTag('candidate-error'):
            self.nominated_cand['peer-cand'] = False
zimio's avatar
zimio committed
            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)
Yann Leboulanger's avatar
Yann Leboulanger committed
                    response.delChild(response.getQuery())
                    self.session.connection.connection.send(response)
                    self.__state_changed(STATE_TRANSFERING)
                args = {'candError' : True}
                self.__state_changed(STATE_CAND_RECEIVED, args)
zimio's avatar
zimio committed
            return
zimio's avatar
zimio committed

        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}
zimio's avatar
zimio committed
        if self.state == STATE_CAND_SENT:
            self.__state_changed(STATE_CAND_SENT_AND_RECEIVED, args)
            response = stanza.buildReply('result')
Yann Leboulanger's avatar
Yann Leboulanger committed
            response.delChild(response.getQuery())
            self.session.connection.connection.send(response)
            self.__state_changed(STATE_TRANSFERING)
Zhenchao Li's avatar
Zhenchao Li committed
        else:
            self.__state_changed(STATE_CAND_RECEIVED, args)
Yann Leboulanger's avatar
Yann Leboulanger committed

    def __on_iq_result(self, stanza, content, error, action):
        log.info("__on_iq_result")
Yann Leboulanger's avatar
Yann Leboulanger committed

        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:
                self.__state_changed(STATE_TRANSPORT_REPLACE)
            self.__state_changed(STATE_TRANSFERING)
Yann Leboulanger's avatar
Yann Leboulanger committed
    def __transport_setup(self, stanza=None, content=None, error=None,
    action=None):
        # Sets up a few transport specific things for the file transfer
Yann Leboulanger's avatar
Yann Leboulanger committed
        if self.transport.type_ == TransportType.IBB:
zimio's avatar
zimio committed
            # No action required, just set the state to transfering
            self.state = STATE_TRANSFERING
    def on_connect(self, streamhost):
Yann Leboulanger's avatar
Yann Leboulanger committed
        log.info('send_candidate_used')
        args = {'streamhost' : streamhost,
                'sendCand'   : True}
        self.nominated_cand['our-cand'] = streamhost
zimio's avatar
zimio committed
        self.__sendCand(args)
zimio's avatar
zimio committed
    def _on_connect_error(self, sid):
        log.info('connect error, sid=' + sid)
zimio's avatar
zimio committed
        args = {'candError' : True,
zimio's avatar
zimio committed
                'sendCand'  : True}
zimio's avatar
zimio committed
        self.__sendCand(args)

    def __sendCand(self, args):
zimio's avatar
zimio committed
        if self.state == STATE_CAND_RECEIVED:
            self.__state_changed(STATE_CAND_SENT_AND_RECEIVED, args)
            self.__state_changed(STATE_CAND_SENT, args)
    def _store_socks5_sid(self, sid, hash_id):
        # callback from socsk5queue.start_listener
zimio's avatar
zimio committed
        self.file_props.hash_ = hash_id
    def _listen_host(self):
zimio's avatar
zimio committed
        receiver = self.file_props.receiver
        sender = self.file_props.sender
        sha_str = helpers.get_auth_sha(self.file_props.sid, sender,
Yann Leboulanger's avatar
Yann Leboulanger committed
            receiver)
zimio's avatar
zimio committed
        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,
Yann Leboulanger's avatar
Yann Leboulanger committed
                self._store_socks5_sid, self.file_props,
Yann Leboulanger's avatar
Yann Leboulanger committed
                fingerprint=fingerprint, typ='sender')
        else:
            listener = gajim.socks5queue.start_listener(port, sha_str,
Yann Leboulanger's avatar
Yann Leboulanger committed
                self._store_socks5_sid, self.file_props,
Yann Leboulanger's avatar
Yann Leboulanger committed
                fingerprint=fingerprint, typ='receiver')

        if not listener:
Yann Leboulanger's avatar
Yann Leboulanger committed
            # send error message, notify the user
            return
        '''
        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'])
zimio's avatar
zimio committed
            return our_pr > peer_pr
            return self.weinitiate
def get_content(desc):
    return JingleFileTransfer

contents[xmpp.NS_JINGLE_FILE_TRANSFER] = get_content