1# Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
2#                    Junglecow J <junglecow AT gmail.com>
3# Copyright (C) 2006-2007 Tomasz Melcer <liori AT exroot.org>
4#                         Travis Shirk <travis AT pobox.com>
5#                         Nikos Kouremenos <kourem AT gmail.com>
6# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
7# Copyright (C) 2007 Julien Pivotto <roidelapluie AT gmail.com>
8# Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com>
9#                         Jean-Marie Traissard <jim AT lapin.org>
10#                         Stephan Erb <steve-e AT h3c.de>
11# Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
12#
13# This file is part of Gajim.
14#
15# Gajim is free software; you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published
17# by the Free Software Foundation; version 3 only.
18#
19# Gajim is distributed in the hope that it will be useful,
20# but WITHOUT ANY WARRANTY; without even the implied warranty of
21# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22# GNU General Public License for more details.
23#
24# You should have received a copy of the GNU General Public License
25# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
26
27import socket
28import logging
29
30import nbxmpp
31from nbxmpp.namespaces import Namespace
32from nbxmpp.structs import StanzaHandler
33from gi.repository import GLib
34
35from gajim.common import app
36from gajim.common import helpers
37from gajim.common import jingle_xtls
38from gajim.common.file_props import FilesProp
39from gajim.common.socks5 import Socks5SenderClient
40from gajim.common.nec import NetworkEvent
41from gajim.common.modules.base import BaseModule
42
43
44log = logging.getLogger('gajim.c.m.bytestream')
45
46def is_transfer_paused(file_props):
47    if file_props.stopped:
48        return False
49    if file_props.completed:
50        return False
51    if file_props.disconnect_cb:
52        return False
53    return file_props.paused
54
55def is_transfer_active(file_props):
56    if file_props.stopped:
57        return False
58    if file_props.completed:
59        return False
60    if not file_props.started:
61        return False
62    if file_props.paused:
63        return True
64    return not file_props.paused
65
66def is_transfer_stopped(file_props):
67    if not file_props:
68        return True
69    if file_props.error:
70        return True
71    if file_props.completed:
72        return True
73    if not file_props.stopped:
74        return False
75    return True
76
77
78class Bytestream(BaseModule):
79    def __init__(self, con):
80        BaseModule.__init__(self, con)
81
82        self.handlers = [
83            StanzaHandler(name='iq',
84                          typ='result',
85                          ns=Namespace.BYTESTREAM,
86                          callback=self._on_bytestream_result),
87            StanzaHandler(name='iq',
88                          typ='error',
89                          ns=Namespace.BYTESTREAM,
90                          callback=self._on_bytestream_error),
91            StanzaHandler(name='iq',
92                          typ='set',
93                          ns=Namespace.BYTESTREAM,
94                          callback=self._on_bytestream_set),
95            StanzaHandler(name='iq',
96                          typ='result',
97                          callback=self._on_result),
98        ]
99
100        self.no_gupnp_reply_id = None
101        self.ok_id = None
102        self.fail_id = None
103
104    def pass_disco(self, info):
105        if Namespace.BYTESTREAM not in info.features:
106            return
107        if app.settings.get_account_setting(self._account, 'use_ft_proxies'):
108            log.info('Discovered proxy: %s', info.jid)
109            our_fjid = self._con.get_own_jid()
110            testit = app.settings.get_account_setting(
111                self._account, 'test_ft_proxies_on_startup')
112            app.proxy65_manager.resolve(
113                info.jid, self._con.connection, str(our_fjid),
114                default=self._account, testit=testit)
115            raise nbxmpp.NodeProcessed
116
117    def _ft_get_receiver_jid(self, file_props):
118        if self._account == 'Local':
119            return file_props.receiver.jid
120        return file_props.receiver.jid + '/' + file_props.receiver.resource
121
122    def _ft_get_from(self, iq_obj):
123        if self._account == 'Local':
124            return iq_obj.getFrom()
125        return helpers.get_full_jid_from_iq(iq_obj)
126
127    def _ft_get_streamhost_jid_attr(self, streamhost):
128        if self._account == 'Local':
129            return streamhost.getAttr('jid')
130        return helpers.parse_jid(streamhost.getAttr('jid'))
131
132    def send_file_approval(self, file_props):
133        """
134        Send iq, confirming that we want to download the file
135        """
136        # user response to ConfirmationDialog may come after we've disconnected
137        if not app.account_is_available(self._account):
138            return
139
140        # file transfer initiated by a jingle session
141        log.info("send_file_approval: jingle session accept")
142
143        session = self._con.get_module('Jingle').get_jingle_session(
144            file_props.sender, file_props.sid)
145        if not session:
146            return
147        content = None
148        for content_ in session.contents.values():
149            if content_.transport.sid == file_props.transport_sid:
150                content = content_
151                break
152
153        if not content:
154            return
155
156        if not session.accepted:
157            content = session.get_content('file', content.name)
158            if content.use_security:
159                fingerprint = content.x509_fingerprint
160                if not jingle_xtls.check_cert(
161                        app.get_jid_without_resource(file_props.sender),
162                        fingerprint):
163                    id_ = jingle_xtls.send_cert_request(
164                        self._con, file_props.sender)
165                    jingle_xtls.key_exchange_pend(id_,
166                                                  content.on_cert_received, [])
167                    return
168            session.approve_session()
169
170        session.approve_content('file', content.name)
171
172    def send_file_rejection(self, file_props):
173        """
174        Inform sender that we refuse to download the file
175
176        typ is used when code = '400', in this case typ can be 'stream' for
177        invalid stream or 'profile' for invalid profile
178        """
179        # user response to ConfirmationDialog may come after we've disconnected
180        if not app.account_is_available(self._account):
181            return
182
183        for session in self._con.get_module('Jingle').get_jingle_sessions(
184                None, file_props.sid):
185            session.cancel_session()
186
187    def send_success_connect_reply(self, streamhost):
188        """
189        Send reply to the initiator of FT that we made a connection
190        """
191        if not app.account_is_available(self._account):
192            return
193        if streamhost is None:
194            return
195        iq = nbxmpp.Iq(to=streamhost['initiator'],
196                       typ='result',
197                       frm=streamhost['target'])
198        iq.setAttr('id', streamhost['id'])
199        query = iq.setTag('query', namespace=Namespace.BYTESTREAM)
200        stream_tag = query.setTag('streamhost-used')
201        stream_tag.setAttr('jid', streamhost['jid'])
202        self._con.connection.send(iq)
203
204    def stop_all_active_file_transfers(self, contact):
205        """
206        Stop all active transfer to or from the given contact
207        """
208        for file_props in FilesProp.getAllFileProp():
209            if is_transfer_stopped(file_props):
210                continue
211            receiver_jid = file_props.receiver
212            if contact.get_full_jid() == receiver_jid:
213                file_props.error = -5
214                self.remove_transfer(file_props)
215                app.nec.push_incoming_event(
216                    NetworkEvent('file-request-error',
217                                 conn=self._con,
218                                 jid=app.get_jid_without_resource(contact.jid),
219                                 file_props=file_props,
220                                 error_msg=''))
221            sender_jid = file_props.sender
222            if contact.get_full_jid() == sender_jid:
223                file_props.error = -3
224                self.remove_transfer(file_props)
225
226    def remove_all_transfers(self):
227        """
228        Stop and remove all active connections from the socks5 pool
229        """
230        for file_props in FilesProp.getAllFileProp():
231            self.remove_transfer(file_props)
232
233    def remove_transfer(self, file_props):
234        if file_props is None:
235            return
236        self.disconnect_transfer(file_props)
237
238    @staticmethod
239    def disconnect_transfer(file_props):
240        if file_props is None:
241            return
242        if file_props.hash_:
243            app.socks5queue.remove_sender(file_props.hash_)
244
245        if file_props.streamhosts:
246            for host in file_props.streamhosts:
247                if 'idx' in host and host['idx'] > 0:
248                    app.socks5queue.remove_receiver(host['idx'])
249                    app.socks5queue.remove_sender(host['idx'])
250
251    def _send_socks5_info(self, file_props):
252        """
253        Send iq for the present streamhosts and proxies
254        """
255        if not app.account_is_available(self._account):
256            return
257        receiver = file_props.receiver
258        sender = file_props.sender
259
260        sha_str = helpers.get_auth_sha(file_props.sid, sender, receiver)
261        file_props.sha_str = sha_str
262
263        port = app.settings.get('file_transfers_port')
264        listener = app.socks5queue.start_listener(
265            port,
266            sha_str,
267            self._result_socks5_sid, file_props)
268        if not listener:
269            file_props.error = -5
270            app.nec.push_incoming_event(
271                NetworkEvent('file-request-error',
272                             conn=self._con,
273                             jid=app.get_jid_without_resource(receiver),
274                             file_props=file_props,
275                             error_msg=''))
276            self._connect_error(file_props.sid,
277                                error='not-acceptable',
278                                error_type='modify')
279        else:
280            iq = nbxmpp.Iq(to=receiver, typ='set')
281            file_props.request_id = 'id_' + file_props.sid
282            iq.setID(file_props.request_id)
283            query = iq.setTag('query', namespace=Namespace.BYTESTREAM)
284            query.setAttr('sid', file_props.sid)
285
286            self._add_addiditional_streamhosts_to_query(query, file_props)
287            self._add_local_ips_as_streamhosts_to_query(query, file_props)
288            self._add_proxy_streamhosts_to_query(query, file_props)
289            self._add_upnp_igd_as_streamhost_to_query(query, file_props, iq)
290            # Upnp-igd is asynchronous, so it will send the iq itself
291
292    @staticmethod
293    def _add_streamhosts_to_query(query, sender, port, hosts):
294        for host in hosts:
295            streamhost = nbxmpp.Node(tag='streamhost')
296            query.addChild(node=streamhost)
297            streamhost.setAttr('port', str(port))
298            streamhost.setAttr('host', host)
299            streamhost.setAttr('jid', sender)
300
301    def _add_local_ips_as_streamhosts_to_query(self, query, file_props):
302        if not app.settings.get_account_setting(self._account,
303                                                'ft_send_local_ips'):
304            return
305
306        my_ip = self._con.local_address
307        if my_ip is None:
308            log.warning('No local address available')
309            return
310
311        try:
312            # The ip we're connected to server with
313            my_ips = [my_ip]
314            # all IPs from local DNS
315            for addr in socket.getaddrinfo(socket.gethostname(), None):
316                if (not addr[4][0] in my_ips and
317                        not addr[4][0].startswith('127') and
318                        not addr[4][0] == '::1'):
319                    my_ips.append(addr[4][0])
320
321            sender = file_props.sender
322            port = app.settings.get('file_transfers_port')
323            self._add_streamhosts_to_query(query, sender, port, my_ips)
324        except socket.gaierror:
325            from gajim.common.connection_handlers_events import InformationEvent
326            app.nec.push_incoming_event(
327                InformationEvent(None, dialog_name='wrong-host'))
328
329    def _add_addiditional_streamhosts_to_query(self, query, file_props):
330        sender = file_props.sender
331        port = app.settings.get('file_transfers_port')
332        ft_add_hosts_to_send = app.settings.get('ft_add_hosts_to_send')
333        add_hosts = []
334        if ft_add_hosts_to_send:
335            add_hosts = [e.strip() for e in ft_add_hosts_to_send.split(',')]
336        else:
337            add_hosts = []
338        self._add_streamhosts_to_query(query, sender, port, add_hosts)
339
340    def _add_upnp_igd_as_streamhost_to_query(self, query, file_props, iq):
341        my_ip = self._con.local_address
342        if my_ip is None or not app.is_installed('UPNP'):
343            log.warning('No local address available')
344            self._con.connection.send(iq)
345            return
346
347        # check if we are connected with an IPv4 address
348        try:
349            socket.inet_aton(my_ip)
350        except socket.error:
351            self._con.connection.send(iq)
352            return
353
354        def ip_is_local(ip):
355            if '.' not in ip:
356                # it's an IPv6
357                return True
358            ip_s = ip.split('.')
359            ip_l = int(ip_s[0])<<24 | int(ip_s[1])<<16 | int(ip_s[2])<<8 | \
360                 int(ip_s[3])
361            # 10/8
362            if ip_l & (255<<24) == 10<<24:
363                return True
364            # 172.16/12
365            if ip_l & (255<<24 | 240<<16) == (172<<24 | 16<<16):
366                return True
367            # 192.168
368            if ip_l & (255<<24 | 255<<16) == (192<<24 | 168<<16):
369                return True
370            return False
371
372        if not ip_is_local(my_ip):
373            self.connection.send(iq)
374            return
375
376        self.no_gupnp_reply_id = 0
377
378        def cleanup_gupnp():
379            if self.no_gupnp_reply_id:
380                GLib.source_remove(self.no_gupnp_reply_id)
381                self.no_gupnp_reply_id = 0
382            app.gupnp_igd.disconnect(self.ok_id)
383            app.gupnp_igd.disconnect(self.fail_id)
384
385        def success(_gupnp, _proto, ext_ip, _re, ext_port,
386                    local_ip, local_port, _desc):
387            log.debug('Got GUPnP-IGD answer: external: %s:%s, internal: %s:%s',
388                      ext_ip, ext_port, local_ip, local_port)
389            if local_port != app.settings.get('file_transfers_port'):
390                sender = file_props.sender
391                receiver = file_props.receiver
392                sha_str = helpers.get_auth_sha(file_props.sid,
393                                               sender,
394                                               receiver)
395                listener = app.socks5queue.start_listener(
396                    local_port,
397                    sha_str,
398                    self._result_socks5_sid,
399                    file_props.sid)
400                if listener:
401                    self._add_streamhosts_to_query(query,
402                                                   sender,
403                                                   ext_port,
404                                                   [ext_ip])
405            else:
406                self._add_streamhosts_to_query(query,
407                                               file_props.sender,
408                                               ext_port,
409                                               [ext_ip])
410            self._con.connection.send(iq)
411            cleanup_gupnp()
412
413        def fail(_gupnp, error, _proto, _ext_ip, _local_ip, _local_port, _desc):
414            log.debug('Got GUPnP-IGD error: %s', error)
415            self._con.connection.send(iq)
416            cleanup_gupnp()
417
418        def no_upnp_reply():
419            log.debug('Got not GUPnP-IGD answer')
420            # stop trying to use it
421            app.disable_dependency('UPNP')
422            self.no_gupnp_reply_id = 0
423            self._con.connection.send(iq)
424            cleanup_gupnp()
425            return False
426
427
428        self.ok_id = app.gupnp_igd.connect('mapped-external-port', success)
429        self.fail_id = app.gupnp_igd.connect('error-mapping-port', fail)
430
431        port = app.settings.get('file_transfers_port')
432        self.no_gupnp_reply_id = GLib.timeout_add_seconds(10, no_upnp_reply)
433        app.gupnp_igd.add_port('TCP',
434                               0,
435                               my_ip,
436                               port,
437                               3600,
438                               'Gajim file transfer')
439
440    def _add_proxy_streamhosts_to_query(self, query, file_props):
441        proxyhosts = self._get_file_transfer_proxies_from_config(file_props)
442        if proxyhosts:
443            file_props.proxy_receiver = file_props.receiver
444            file_props.proxy_sender = file_props.sender
445            file_props.proxyhosts = proxyhosts
446
447            for proxyhost in proxyhosts:
448                self._add_streamhosts_to_query(query,
449                                               proxyhost['jid'],
450                                               proxyhost['port'],
451                                               [proxyhost['host']])
452
453    def _get_file_transfer_proxies_from_config(self, file_props):
454        configured_proxies = app.settings.get_account_setting(
455            self._account, 'file_transfer_proxies')
456        shall_use_proxies = app.settings.get_account_setting(
457            self._account, 'use_ft_proxies')
458        if shall_use_proxies:
459            proxyhost_dicts = []
460            proxies = []
461            if configured_proxies:
462                proxies = [item.strip() for item in
463                           configured_proxies.split(',')]
464            default_proxy = app.proxy65_manager.get_default_for_name(
465                self._account)
466            if default_proxy:
467                # add/move default proxy at top of the others
468                if default_proxy in proxies:
469                    proxies.remove(default_proxy)
470                proxies.insert(0, default_proxy)
471
472            for proxy in proxies:
473                (host, _port, jid) = app.proxy65_manager.get_proxy(
474                    proxy, self._account)
475                if not host:
476                    continue
477                host_dict = {
478                    'state': 0,
479                    'target': file_props.receiver,
480                    'id': file_props.sid,
481                    'sid': file_props.sid,
482                    'initiator': proxy,
483                    'host': host,
484                    'port': str(_port),
485                    'jid': jid
486                }
487                proxyhost_dicts.append(host_dict)
488            return proxyhost_dicts
489
490        return []
491
492    @staticmethod
493    def _result_socks5_sid(sid, hash_id):
494        """
495        Store the result of SHA message from auth
496        """
497        file_props = FilesProp.getFilePropBySid(sid)
498        file_props.hash_ = hash_id
499
500    def _connect_error(self, sid, error, error_type, msg=None):
501        """
502        Called when there is an error establishing BS connection, or when
503        connection is rejected
504        """
505        if not app.account_is_available(self._account):
506            return
507        file_props = FilesProp.getFileProp(self._account, sid)
508        if file_props is None:
509            log.error('can not send iq error on failed transfer')
510            return
511        if file_props.type_ == 's':
512            to = file_props.receiver
513        else:
514            to = file_props.sender
515        iq = nbxmpp.Iq(to=to, typ='error')
516        iq.setAttr('id', file_props.request_id)
517        err = iq.setTag('error')
518        err.setAttr('type', error_type)
519        err.setTag(error, namespace=Namespace.STANZAS)
520        self._con.connection.send(iq)
521        if msg:
522            self.disconnect_transfer(file_props)
523            file_props.error = -3
524            app.nec.push_incoming_event(
525                NetworkEvent('file-request-error',
526                             conn=self._con,
527                             jid=app.get_jid_without_resource(to),
528                             file_props=file_props,
529                             error_msg=msg))
530
531    def _proxy_auth_ok(self, proxy):
532        """
533        Called after authentication to proxy server
534        """
535        if not app.account_is_available(self._account):
536            return
537        file_props = FilesProp.getFileProp(self._account, proxy['sid'])
538        iq = nbxmpp.Iq(to=proxy['initiator'], typ='set')
539        auth_id = "au_" + proxy['sid']
540        iq.setID(auth_id)
541        query = iq.setTag('query', namespace=Namespace.BYTESTREAM)
542        query.setAttr('sid', proxy['sid'])
543        activate = query.setTag('activate')
544        activate.setData(file_props.proxy_receiver)
545        iq.setID(auth_id)
546        self._con.connection.send(iq)
547
548    def _on_bytestream_error(self, _con, iq_obj, _properties):
549        id_ = iq_obj.getAttr('id')
550        frm = helpers.get_full_jid_from_iq(iq_obj)
551        query = iq_obj.getTag('query')
552        app.proxy65_manager.error_cb(frm, query)
553        jid = helpers.get_jid_from_iq(iq_obj)
554        id_ = id_[3:]
555        file_props = FilesProp.getFilePropBySid(id_)
556        if not file_props:
557            return
558        file_props.error = -4
559        app.nec.push_incoming_event(
560            NetworkEvent('file-request-error',
561                         conn=self._con,
562                         jid=app.get_jid_without_resource(jid),
563                         file_props=file_props,
564                         error_msg=''))
565        raise nbxmpp.NodeProcessed
566
567    def _on_bytestream_set(self, con, iq_obj, _properties):
568        target = iq_obj.getAttr('to')
569        id_ = iq_obj.getAttr('id')
570        query = iq_obj.getTag('query')
571        sid = query.getAttr('sid')
572        file_props = FilesProp.getFileProp(self._account, sid)
573        streamhosts = []
574        for item in query.getChildren():
575            if item.getName() == 'streamhost':
576                host_dict = {
577                    'state': 0,
578                    'target': target,
579                    'id': id_,
580                    'sid': sid,
581                    'initiator': self._ft_get_from(iq_obj)
582                }
583                for attr in item.getAttrs():
584                    host_dict[attr] = item.getAttr(attr)
585                if 'host' not in host_dict:
586                    continue
587                if 'jid' not in host_dict:
588                    continue
589                if 'port' not in host_dict:
590                    continue
591                streamhosts.append(host_dict)
592        file_props = FilesProp.getFilePropBySid(sid)
593        if file_props is not None:
594            if file_props.type_ == 's': # FIXME: remove fast xmlns
595                # only psi do this
596                if file_props.streamhosts:
597                    file_props.streamhosts.extend(streamhosts)
598                else:
599                    file_props.streamhosts = streamhosts
600                app.socks5queue.connect_to_hosts(
601                    self._account,
602                    sid,
603                    self.send_success_connect_reply,
604                    None)
605                raise nbxmpp.NodeProcessed
606        else:
607            log.warning('Gajim got streamhosts for unknown transfer. '
608                        'Ignoring it.')
609            raise nbxmpp.NodeProcessed
610
611        file_props.streamhosts = streamhosts
612        def _connection_error(sid):
613            self._connect_error(sid,
614                                'item-not-found',
615                                'cancel',
616                                msg='Could not connect to given hosts')
617        if file_props.type_ == 'r':
618            app.socks5queue.connect_to_hosts(
619                self._account,
620                sid,
621                self.send_success_connect_reply,
622                _connection_error)
623        raise nbxmpp.NodeProcessed
624
625    def _on_result(self, _con, iq_obj, _properties):
626        # if we want to respect xep-0065 we have to check for proxy
627        # activation result in any result iq
628        real_id = iq_obj.getAttr('id')
629        if real_id is None:
630            log.warning('Invalid IQ without id attribute:\n%s', iq_obj)
631            raise nbxmpp.NodeProcessed
632        if real_id is None or not real_id.startswith('au_'):
633            return
634        frm = self._ft_get_from(iq_obj)
635        id_ = real_id[3:]
636        file_props = FilesProp.getFilePropByTransportSid(self._account, id_)
637        if file_props.streamhost_used:
638            for host in file_props.proxyhosts:
639                if host['initiator'] == frm and 'idx' in host:
640                    app.socks5queue.activate_proxy(host['idx'])
641                    raise nbxmpp.NodeProcessed
642
643    def _on_bytestream_result(self, con, iq_obj, _properties):
644        frm = self._ft_get_from(iq_obj)
645        real_id = iq_obj.getAttr('id')
646        query = iq_obj.getTag('query')
647        app.proxy65_manager.resolve_result(frm, query)
648
649        try:
650            streamhost = query.getTag('streamhost-used')
651        except Exception: # this bytestream result is not what we need
652            pass
653        id_ = real_id[3:]
654        file_props = FilesProp.getFileProp(self._account, id_)
655        if file_props is None:
656            raise nbxmpp.NodeProcessed
657        if streamhost is None:
658            # proxy approves the activate query
659            if real_id.startswith('au_'):
660                if file_props.streamhost_used is False:
661                    raise nbxmpp.NodeProcessed
662                if  not file_props.proxyhosts:
663                    raise nbxmpp.NodeProcessed
664                for host in file_props.proxyhosts:
665                    if host['initiator'] == frm and \
666                    query.getAttr('sid') == file_props.sid:
667                        app.socks5queue.activate_proxy(host['idx'])
668                        break
669            raise nbxmpp.NodeProcessed
670        jid = self._ft_get_streamhost_jid_attr(streamhost)
671        if file_props.streamhost_used is True:
672            raise nbxmpp.NodeProcessed
673
674        if real_id.startswith('au_'):
675            if file_props.stopped:
676                self.remove_transfer(file_props)
677            else:
678                app.socks5queue.send_file(file_props, self._account, 'server')
679            raise nbxmpp.NodeProcessed
680
681        proxy = None
682        if file_props.proxyhosts:
683            for proxyhost in file_props.proxyhosts:
684                if proxyhost['jid'] == jid:
685                    proxy = proxyhost
686
687        if file_props.stopped:
688            self.remove_transfer(file_props)
689            raise nbxmpp.NodeProcessed
690        if proxy is not None:
691            file_props.streamhost_used = True
692            file_props.streamhosts.append(proxy)
693            file_props.is_a_proxy = True
694            idx = app.socks5queue.idx
695            sender = Socks5SenderClient(app.idlequeue,
696                                        idx,
697                                        app.socks5queue,
698                                        _sock=None,
699                                        host=str(proxy['host']),
700                                        port=int(proxy['port']),
701                                        fingerprint=None,
702                                        connected=False,
703                                        file_props=file_props)
704            sender.streamhost = proxy
705            app.socks5queue.add_sockobj(self._account, sender)
706            proxy['idx'] = sender.queue_idx
707            app.socks5queue.on_success[file_props.sid] = self._proxy_auth_ok
708            raise nbxmpp.NodeProcessed
709
710        if file_props.stopped:
711            self.remove_transfer(file_props)
712        else:
713            app.socks5queue.send_file(file_props, self._account, 'server')
714
715        raise nbxmpp.NodeProcessed
716
717
718def get_instance(*args, **kwargs):
719    return Bytestream(*args, **kwargs), 'Bytestream'
720