gajim-plural/gajim/common/jingle_ft.py

408 lines
17 KiB
Python

# 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 hashlib
import logging
import os
import threading
from enum import IntEnum, unique
import nbxmpp
from gajim.common import app
from gajim.common import configpaths
from gajim.common import jingle_xtls
from gajim.common.jingle_content import contents, JingleContent
from gajim.common.jingle_transport import JingleTransportSocks5, TransportType
from gajim.common import helpers
from gajim.common.connection_handlers_events import FileRequestReceivedEvent
from gajim.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, senders=None):
JingleContent.__init__(self, session, transport, senders)
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 app.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)
}
cert_name = os.path.join(configpaths.get('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")
app.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_5)
self.session.__session_info(checksum)
pjid = app.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.Hashes2()
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(app.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):
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:
sid = self.file_props.transport_sid
app.socks5queue.connect_to_hosts(self.session.connection.name,
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 app.socks5queue.listener.connections:
app.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 = {'candError' : 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 app.socks5queue.listener and \
not app.socks5queue.listener.connections:
app.socks5queue.listener.disconnect()
if content.getTag('transport').getTag('activated'):
self.state = State.TRANSFERING
app.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
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=%s', 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 = app.config.get('file_transfers_port')
fingerprint = None
if self.use_security:
fingerprint = 'server'
listener = app.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_5] = get_content