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