jingle_ft.py 17.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
# -*- 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)
"""

22
import hashlib
23 24 25
import logging
import os
import threading
26
from enum import IntEnum, unique
27
import nbxmpp
28
from common import gajim
29
from common import configpaths
30
from common import jingle_xtls
31
from common.jingle_content import contents, JingleContent
32
from common.jingle_transport import JingleTransportSocks5, TransportType
33
from common import helpers
34
from common.connection_handlers_events import FileRequestReceivedEvent
35 36 37 38
from common.jingle_ftstates import (
    StateInitialized, StateCandSent, StateCandReceived, StateTransfering,
    StateCandSentAndRecv, StateTransportReplace)

39 40
log = logging.getLogger('gajim.c.jingle_ft')

41

42
@unique
43 44 45 46 47 48 49 50 51 52 53 54 55
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
56

57 58

class JingleFileTransfer(JingleContent):
59

Yann Leboulanger's avatar
Yann Leboulanger committed
60
    def __init__(self, session, transport=None, file_props=None,
61
                 use_security=False):
62
        JingleContent.__init__(self, session, transport)
63
        log.info("transport value: %s", transport)
Yann Leboulanger's avatar
Yann Leboulanger committed
64
        # events we might be interested in
65
        self.callbacks['session-initiate'] += [self.__on_session_initiate]
Yann Leboulanger's avatar
Yann Leboulanger committed
66 67
        self.callbacks['session-initiate-sent'] += [
            self.__on_session_initiate_sent]
68
        self.callbacks['content-add'] += [self.__on_session_initiate]
69
        self.callbacks['session-accept'] += [self.__on_session_accept]
Yann Leboulanger's avatar
Yann Leboulanger committed
70
        self.callbacks['session-terminate'] += [self.__on_session_terminate]
zimio's avatar
zimio committed
71
        self.callbacks['session-info'] += [self.__on_session_info]
72
        self.callbacks['transport-accept'] += [self.__on_transport_accept]
Yann Leboulanger's avatar
Yann Leboulanger committed
73
        self.callbacks['transport-replace'] += [self.__on_transport_replace]
74
        self.callbacks['session-accept-sent'] += [self.__transport_setup]
Yann Leboulanger's avatar
Yann Leboulanger committed
75
        # fallback transport method
76 77
        self.callbacks['transport-reject'] += [self.__on_transport_reject]
        self.callbacks['transport-info'] += [self.__on_transport_info]
78
        self.callbacks['iq-result'] += [self.__on_iq_result]
79
        self.use_security = use_security
80
        self.x509_fingerprint = None
81
        self.file_props = file_props
zimio's avatar
zimio committed
82
        self.weinitiate = self.session.weinitiate
83
        self.werequest = self.session.werequest
84
        if self.file_props is not None:
zimio's avatar
zimio committed
85 86 87 88 89 90
            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
zimio's avatar
zimio committed
91
            self.file_props.session_type = 'jingle'
92
            self.file_props.sid = session.sid
zimio's avatar
zimio committed
93
            self.file_props.transfered_size = []
94
            self.file_props.transport_sid = self.transport.sid
95
        log.info("FT request: %s", file_props)
96
        if transport is None:
97
            self.transport = JingleTransportSocks5()
zimio's avatar
zimio committed
98 99 100
        self.transport.set_connection(session.connection)
        self.transport.set_file_props(self.file_props)
        self.transport.set_our_jid(session.ourjid)
101
        log.info('ourjid: %s', session.ourjid)
102
        self.session = session
103
        self.media = 'file'
104
        self.nominated_cand = {}
105
        if gajim.contacts.is_gc_contact(session.connection.name,
106
                                        session.peerjid):
107
            roomjid = session.peerjid.split('/')[0]
108
            dstaddr = hashlib.sha1(('%s%s%s' % (self.file_props.sid,
109 110
                                                session.ourjid, roomjid))
                                   .encode('utf-8')).hexdigest()
111
            self.file_props.dstaddr = dstaddr
112 113 114 115 116 117 118 119
        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
120
        }
121

122 123 124 125 126 127
        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')
128 129 130

    def __state_changed(self, nextstate, args=None):
        # Executes the next state action and sets the next state
131
        current_state = self.state
132 133
        st = self.states[nextstate]
        st.action(args)
134 135 136
        # 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
137

138
    def __on_session_initiate(self, stanza, content, error, action):
139
        log.debug("Jingle FT request received")
140
        gajim.nec.push_incoming_event(FileRequestReceivedEvent(None,
141 142 143 144
                                                               conn=self.session.connection,
                                                               stanza=stanza,
                                                               jingle_content=content,
                                                               FT_content=self))
145 146 147 148
        if self.session.request:
            # accept the request
            self.session.approve_content(self.media, self.name)
            self.session.accept_session()
zimio's avatar
zimio committed
149

zimio's avatar
zimio committed
150
    def __on_session_initiate_sent(self, stanza, content, error, action):
151
        pass
Yann Leboulanger's avatar
Yann Leboulanger committed
152

153 154
    def __send_hash(self):
        # Send hash in a session info
155 156 157
        checksum = nbxmpp.Node(tag='checksum',
                               payload=[nbxmpp.Node(tag='file',
                                                    payload=[self._compute_hash()])])
158
        checksum.setNamespace(nbxmpp.NS_JINGLE_FILE_TRANSFER)
159
        self.session.__session_info(checksum)
160
        pjid = gajim.get_jid_without_resource(self.session.peerjid)
161
        file_info = {'name' : self.file_props.name,
162
                     'file-name' : self.file_props.file_name,
163 164
                     'hash' : self.file_props.hash_,
                     'size' : self.file_props.size,
165 166
                     'date' : self.file_props.date,
                     'peerjid' : pjid
167
                    }
zimio's avatar
zimio committed
168
        self.session.connection.set_file_info(file_info)
169

170
    def _compute_hash(self):
171
        # Caculates the hash and returns a xep-300 hash stanza
172
        if self.file_props.algo is None:
zimio's avatar
zimio committed
173 174
            return
        try:
175
            file_ = open(self.file_props.file_name, 'rb')
176
        except IOError:
177
            # can't open file
zimio's avatar
zimio committed
178
            return
179
        h = nbxmpp.Hashes()
zimio's avatar
zimio committed
180
        hash_ = h.calculateHash(self.file_props.algo, file_)
181
        file_.close()
182
        # DEBUG
zimio's avatar
zimio committed
183
        #hash_ = '1294809248109223'
184 185 186
        if not hash_:
            # Hash alogrithm not supported
            return
zimio's avatar
zimio committed
187
        self.file_props.hash_ = hash_
zimio's avatar
zimio committed
188
        h.addHash(hash_, self.file_props.algo)
189
        return h
Yann Leboulanger's avatar
Yann Leboulanger committed
190

191 192 193 194
    def on_cert_received(self):
        self.session.approve_session()
        self.session.approve_content('file', name=self.name)

195 196
    def __on_session_accept(self, stanza, content, error, action):
        log.info("__on_session_accept")
197
        con = self.session.connection
198 199 200
        security = content.getTag('security')
        if not security: # responder can not verify our fingerprint
            self.use_security = False
201 202 203 204 205 206
        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(
207
                        self.session.responder), fingerprint):
208
                    id_ = jingle_xtls.send_cert_request(con,
209
                                                        self.session.responder)
210
                    jingle_xtls.key_exchange_pend(id_,
211 212
                                                  self.continue_session_accept,
                                                  [stanza])
213 214 215 216 217
                    raise nbxmpp.NodeProcessed
        self.continue_session_accept(stanza)

    def continue_session_accept(self, stanza):
        con = self.session.connection
218
        if self.state == State.TRANSPORT_REPLACE:
zimio's avatar
zimio committed
219 220
            # If we are requesting we don't have the file
            if self.session.werequest:
221
                raise nbxmpp.NodeProcessed
zimio's avatar
zimio committed
222
            # We send the file
223
            self.__state_changed(State.TRANSFERING)
224
            raise nbxmpp.NodeProcessed
zimio's avatar
zimio committed
225
        self.file_props.streamhosts = self.transport.remote_candidates
226 227
        # Calculate file hash in a new thread
        # if we haven't sent the hash already.
228
        if self.file_props.hash_ is None and self.file_props.algo and \
229 230 231
                not self.werequest:
            self.hash_thread = threading.Thread(target=self.__send_hash)
            self.hash_thread.start()
zimio's avatar
zimio committed
232
        for host in self.file_props.streamhosts:
Yann Leboulanger's avatar
Yann Leboulanger committed
233 234
            host['initiator'] = self.session.initiator
            host['target'] = self.session.responder
zimio's avatar
zimio committed
235
            host['sid'] = self.file_props.sid
236 237 238
        fingerprint = None
        if self.use_security:
            fingerprint = 'client'
Yann Leboulanger's avatar
Yann Leboulanger committed
239
        if self.transport.type_ == TransportType.SOCKS5:
240
            gajim.socks5queue.connect_to_hosts(self.session.connection.name,
241 242 243 244 245
                                               self.file_props.sid,
                                               self.on_connect,
                                               self._on_connect_error,
                                               fingerprint=fingerprint,
                                               receiving=False)
246
            raise nbxmpp.NodeProcessed
247
        self.__state_changed(State.TRANSFERING)
248
        raise nbxmpp.NodeProcessed
249 250 251 252

    def __on_session_terminate(self, stanza, content, error, action):
        log.info("__on_session_terminate")

zimio's avatar
zimio committed
253 254
    def __on_session_info(self, stanza, content, error, action):
        pass
Yann Leboulanger's avatar
Yann Leboulanger committed
255

256 257 258 259 260
    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
261

262 263 264 265 266
    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")
267 268 269 270
        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:
271
            raise nbxmpp.NodeProcessed
272
        if cand_error:
273 274
            if not gajim.socks5queue.listener.connections:
                gajim.socks5queue.listener.disconnect()
275
            self.nominated_cand['peer-cand'] = False
276
            if self.state == State.CAND_SENT:
277 278 279 280
                if not self.nominated_cand['our-cand'] and \
                   not self.nominated_cand['peer-cand']:
                    if not self.weinitiate:
                        return
281
                    self.__state_changed(State.TRANSPORT_REPLACE)
282 283
                else:
                    response = stanza.buildReply('result')
284
                    response.delChild(response.getQuery())
285
                    self.session.connection.connection.send(response)
286
                    self.__state_changed(State.TRANSFERING)
287
                    raise nbxmpp.NodeProcessed
288
            else:
289 290
                args = {'cand_error' : True}
                self.__state_changed(State.CAND_RECEIVED, args)
zimio's avatar
zimio committed
291
            return
292 293
        if cand_used:
            streamhost_cid = cand_used.getAttr('cid')
294 295 296 297 298
            streamhost_used = None
            for cand in self.transport.candidates:
                if cand['candidate_id'] == streamhost_cid:
                    streamhost_used = cand
                    break
299
            if streamhost_used is None or streamhost_used['type'] == 'proxy':
300 301
                if gajim.socks5queue.listener and \
                not gajim.socks5queue.listener.connections:
302
                    gajim.socks5queue.listener.disconnect()
zimio's avatar
zimio committed
303
        if content.getTag('transport').getTag('activated'):
304
            self.state = State.TRANSFERING
zimio's avatar
zimio committed
305 306
            jid = gajim.get_jid_without_resource(self.session.ourjid)
            gajim.socks5queue.send_file(self.file_props,
307
                                        self.session.connection.name, 'client')
zimio's avatar
zimio committed
308
            return
309 310 311 312 313
        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)
314
            raise nbxmpp.NodeProcessed
Zhenchao Li's avatar
Zhenchao Li committed
315
        else:
316
            self.__state_changed(State.CAND_RECEIVED, args)
317

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

321 322 323
        if self.state == State.NOT_STARTED:
            self.__state_changed(State.INITIALIZED)
        elif self.state == State.CAND_SENT_AND_RECEIVED:
324
            if not self.nominated_cand['our-cand'] and \
325 326
            not self.nominated_cand['peer-cand']:
                if not self.weinitiate:
327
                    return
328
                self.__state_changed(State.TRANSPORT_REPLACE)
329
                return
330
            # initiate transfer
331
            self.__state_changed(State.TRANSFERING)
Yann Leboulanger's avatar
Yann Leboulanger committed
332

Yann Leboulanger's avatar
Yann Leboulanger committed
333
    def __transport_setup(self, stanza=None, content=None, error=None,
334
                          action=None):
335
        # Sets up a few transport specific things for the file transfer
Yann Leboulanger's avatar
Yann Leboulanger committed
336
        if self.transport.type_ == TransportType.IBB:
zimio's avatar
zimio committed
337
            # No action required, just set the state to transfering
338
            self.state = State.TRANSFERING
339 340
        else:
            self._listen_host()
Yann Leboulanger's avatar
Yann Leboulanger committed
341

342
    def on_connect(self, streamhost):
343 344 345
        """
        send candidate-used stanza
        """
Yann Leboulanger's avatar
Yann Leboulanger committed
346
        log.info('send_candidate_used')
347 348
        if streamhost is None:
            return
349 350
        args = {'streamhost' : streamhost,
                'sendCand'   : True}
351
        self.nominated_cand['our-cand'] = streamhost
352
        self.__send_candidate(args)
353

zimio's avatar
zimio committed
354
    def _on_connect_error(self, sid):
355
        log.info('connect error, sid=' + sid)
zimio's avatar
zimio committed
356
        args = {'candError' : True,
zimio's avatar
zimio committed
357
                'sendCand'  : True}
358
        self.__send_candidate(args)
zimio's avatar
zimio committed
359

360 361 362
    def __send_candidate(self, args):
        if self.state == State.CAND_RECEIVED:
            self.__state_changed(State.CAND_SENT_AND_RECEIVED, args)
363
        else:
364
            self.__state_changed(State.CAND_SENT, args)
365

366 367
    def _store_socks5_sid(self, sid, hash_id):
        # callback from socsk5queue.start_listener
zimio's avatar
zimio committed
368
        self.file_props.hash_ = hash_id
369

370
    def _listen_host(self):
zimio's avatar
zimio committed
371 372 373
        receiver = self.file_props.receiver
        sender = self.file_props.sender
        sha_str = helpers.get_auth_sha(self.file_props.sid, sender,
374
                                       receiver)
zimio's avatar
zimio committed
375
        self.file_props.sha_str = sha_str
376 377 378 379
        port = gajim.config.get('file_transfers_port')
        fingerprint = None
        if self.use_security:
            fingerprint = 'server'
380 381 382 383 384
        listener = gajim.socks5queue.start_listener(port, sha_str,
                                                    self._store_socks5_sid,
                                                    self.file_props,
                                                    fingerprint=fingerprint,
                                                    typ='sender' if self.weinitiate else 'receiver')
385
        if not listener:
Yann Leboulanger's avatar
Yann Leboulanger committed
386
            # send error message, notify the user
387
            return
zimio's avatar
zimio committed
388

389
    def is_our_candidate_used(self):
390 391 392 393
        '''
        If this method returns true then the candidate we nominated will be
        used, if false, the candidate nominated by peer will be used
        '''
394

395
        if not self.nominated_cand['peer-cand']:
396
            return True
397
        if not self.nominated_cand['our-cand']:
398 399 400 401
            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:
zimio's avatar
zimio committed
402
            return our_pr > peer_pr
403
        return self.weinitiate
404

405
    def start_ibb_transfer(self):
zimio's avatar
zimio committed
406
        if self.file_props.type_ == 's':
407
            self.__state_changed(State.TRANSFERING)
zimio's avatar
zimio committed
408

409

410 411 412
def get_content(desc):
    return JingleFileTransfer

413
contents[nbxmpp.NS_JINGLE_FILE_TRANSFER] = get_content