From b90e54692767d1eddd104bfdf935a871609017fd Mon Sep 17 00:00:00 2001 From: Dimitur Kirov Date: Sun, 19 Mar 2006 20:43:30 +0000 Subject: [PATCH] proxy65_manager resolves socks5 proxies at gajim startup and account login --- src/common/connection_handlers.py | 55 ++----- src/common/proxy65_manager.py | 262 ++++++++++++++++++++++++++++++ src/gajim.py | 2 + 3 files changed, 274 insertions(+), 45 deletions(-) create mode 100644 src/common/proxy65_manager.py diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 9a42a30da..276afb922 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -97,23 +97,6 @@ class ConnectionBytestream: gajim.socks5queue.remove_receiver(host['idx']) gajim.socks5queue.remove_sender(host['idx']) - def get_cached_proxies(self, proxy): - ''' get cached entries for proxy and request the cache again ''' - host = gajim.config.get_per('ft_proxies65_cache', proxy, 'host') - port = gajim.config.get_per('ft_proxies65_cache', proxy, 'port') - jid = gajim.config.get_per('ft_proxies65_cache', proxy, 'jid') - - iq = common.xmpp.Protocol(name = 'iq', to = proxy, typ = 'get') - query = iq.setTag('query') - query.setNamespace(common.xmpp.NS_BYTESTREAM) - # FIXME bad logic - this should be somewhere else! - # this line should be put somewhere else - # self.connection.send(iq) - # ensure that we don;t return empty vars - if None not in (host, port, jid) or '' not in (host, port, jid): - return (host, port, jid) - return (None, None, None) - def send_socks5_info(self, file_props, fast = True, receiver = None, sender = None): ''' send iq for the present streamhosts and proxies ''' @@ -131,7 +114,7 @@ class ConnectionBytestream: if fast and cfg_proxies: proxies = map(lambda e:e.strip(), cfg_proxies.split(',')) for proxy in proxies: - (host, _port, jid) = self.get_cached_proxies(proxy) + (host, _port, jid) = gajim.proxy65_manager.get_proxy(proxy) if host is None: continue host_dict={ @@ -361,32 +344,8 @@ class ConnectionBytestream: frm = helpers.get_full_jid_from_iq(iq_obj) real_id = unicode(iq_obj.getAttr('id')) query = iq_obj.getTag('query') - streamhost = None - try: - streamhost = query.getTag('streamhost') - except: - pass - if streamhost is not None: # this is a result for proxy request - jid = None - try: - jid = streamhost.getAttr('jid') - except: - raise common.xmpp.NodeProcessed - proxyhosts = [] - for item in query.getChildren(): - if item.getName() == 'streamhost': - host = item.getAttr('host') - port = item.getAttr('port') - jid = item.getAttr('jid') - conf = gajim.config - conf.add_per('ft_proxies65_cache', jid) - conf.set_per('ft_proxies65_cache', jid, - 'host', unicode(host)) - conf.set_per('ft_proxies65_cache', jid, - 'port', int(port)) - conf.set_per('ft_proxies65_cache', jid, - 'jid', unicode(jid)) - raise common.xmpp.NodeProcessed + gajim.proxy65_manager.resolve_result(frm, query) + try: streamhost = query.getTag('streamhost-used') except: # this bytestream result is not what we need @@ -1521,7 +1480,13 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco) if not self.connection: return self.connection.getRoster(self._on_roster_set) - + cfg_proxies = gajim.config.get_per('accounts', self.name, + 'file_transfer_proxies') + if cfg_proxies: + proxies = map(lambda e:e.strip(), cfg_proxies.split(',')) + for proxy in proxies: + gajim.proxy65_manager.resolve(proxy, self.connection) + def _on_roster_set(self, roster): raw_roster = roster.getRaw() roster = {} diff --git a/src/common/proxy65_manager.py b/src/common/proxy65_manager.py new file mode 100644 index 000000000..7d130bbe5 --- /dev/null +++ b/src/common/proxy65_manager.py @@ -0,0 +1,262 @@ +## +## Copyright (C) 2006 Gajim Team +## +## Contributors for this file: +## - Dimitur Kirov +## +## This program 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 2 only. +## +## This program 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. +## +import socket +import struct + +import common.xmpp +from common import gajim +from socks5 import Socks5 +from common.xmpp.idlequeue import IdleObject + +S_INITIAL = 0 +S_STARTED = 1 +S_RESOLVED = 2 +S_FINISHED = 4 + +CONNECT_TIMEOUT = 20 + +class Proxy65Manager: + ''' keep records for file transfer proxies. Each time account + establishes a connection to its server call proxy65manger.resolve(proxy) + for every proxy that is convigured within the account. The class takes + care to resolve and test each proxy only once.''' + def __init__(self, idlequeue): + # dict {proxy: proxy properties} + self.idlequeue = idlequeue + self.proxies = {} + + def resolve(self, proxy, connection): + ''' start ''' + if self.proxies.has_key(proxy): + resolver = self.proxies[proxy] + else: + # proxy is being ressolved for the first time + resolver = ProxyResolver(proxy) + self.proxies[proxy] = resolver + resolver.add_connection(connection) + + if resolver.state == S_FINISHED: + # resolving this proxy is already started or completed + return + + def diconnect(self, connection): + for resolver in self.proxies: + resolver.disconnect(connection) + + def resolve_result(self, proxy, query): + if not self.proxies.has_key(proxy): + return + jid = None + for item in query.getChildren(): + if item.getName() == 'streamhost': + host = item.getAttr('host') + port = item.getAttr('port') + jid = item.getAttr('jid') + self.proxies[proxy].resolve_result(host, port, jid) + # we can have only one streamhost + raise common.xmpp.NodeProcessed + def get_proxy(self, proxy): + if self.proxies.has_key(proxy): + resolver = self.proxies[proxy] + if resolver.state == S_FINISHED: + return (resolver.host, resolver.port, resolver.jid) + return (None, 0, None) + +class ProxyResolver: + def resolve_result(self, host, port, jid): + ''' test if host has a real proxy65 listening on port ''' + self.host = unicode(host) + self.port = int(port) + self.jid = unicode(jid) + self.state = S_RESOLVED + self.host_tester = HostTester(self.host, self.port, self.jid, + self._on_connect_success, self._on_connect_failure) + self.host_tester.connect() + + def _on_connect_success(self): + conf = gajim.config + conf.add_per('ft_proxies65_cache', self.proxy) + conf.set_per('ft_proxies65_cache', self.proxy, 'host', self.host) + conf.set_per('ft_proxies65_cache', self.proxy, 'port', self.port) + conf.set_per('ft_proxies65_cache', self.proxy, 'jid', self.jid) + self.state = S_FINISHED + + def _on_connect_failure(self): + self.state = S_FINISHED + self.host = None + self.port = 0 + self.jid = None + + def disconnect(self, connection): + if self.host_tester: + self.host_tester.disconnect() + self.host_tester = None + if self.connections.has_key(connection): + self.connections.remove(connection) + if self.state == S_STARTED: + self.state = S_INITIAL + self.try_next_connection() + + def try_next_connection(self): + ''' try to resolve proxy with the next possible connection ''' + if self.connections: + connection = self.connections.pop(0) + self.start_resolve(connection) + + def add_connection(self, connection): + ''' add a new connection in case the first fails ''' + self.connections.append(connection) + if self.state == S_INITIAL: + self.start_resolve(connection) + + def start_resolve(self, connection): + ''' request network address from proxy ''' + self.state = S_STARTED + self.active_connection = connection + iq = common.xmpp.Protocol(name = 'iq', to = self.proxy, typ = 'get') + query = iq.setTag('query') + query.setNamespace(common.xmpp.NS_BYTESTREAM) + connection.send(iq) + + def __init__(self, proxy): + self.proxy = proxy + self.state = S_INITIAL + self.connections = [] + self.host_tester = None + self.jid = None + self.host = None + self.port = None + +class HostTester(Socks5, IdleObject): + ''' fake proxy tester. ''' + def __init__(self, host, port, jid, on_success, on_failure): + ''' try to establish and auth to proxy at (host, port) + call on_success, or on_failure according to the result''' + self.host = host + self.port = port + self.jid = jid + self.on_success = on_success + self.on_failure = on_failure + self._sock = None + self.file_props = {} + Socks5.__init__(self, gajim.idlequeue, host, port, None, None, None) + + def connect(self): + ''' create the socket and plug it to the idlequeue ''' + self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._sock.setblocking(False) + self.fd = self._sock.fileno() + self.state = 0 # about to be connected + gajim.idlequeue.plug_idle(self, True, False) + self.do_connect() + self.idlequeue.set_read_timeout(self.fd, CONNECT_TIMEOUT) + return None + + def read_timeout(self): + self.idlequeue.remove_timeout(self.fd) + self.pollend() + + def pollend(self): + self.disconnect() + self.on_failure() + + def pollout(self): + self.idlequeue.remove_timeout(self.fd) + if self.state == 0: + self.do_connect() + return + elif self.state == 1: # send initially: version and auth types + data = self._get_auth_buff() + self.send_raw(data) + elif self.state == 3: # send 'connect' request + data = self._get_request_buff(self._get_sha1_auth()) + self.send_raw(data) + else: + return + self.state += 1 + # unplug and plug for reading + gajim.idlequeue.plug_idle(self, False, True) + gajim.idlequeue.set_read_timeout(self.fd, CONNECT_TIMEOUT) + + def pollin(self): + self.idlequeue.remove_timeout(self.fd) + if self.state > 1: + self.idlequeue.set_read_timeout(self.fd, CONNECT_TIMEOUT) + result = self.main(0) + else: + self.disconnect() + + def main(self, timeout = 0): + ''' begin negotiation. on success 'address' != 0 ''' + result = 1 + buff = self.receive() + if buff == '': + # end connection + self.pollend() + return + + if self.state == 2: # read auth response + if buff is None or len(buff) != 2: + return None + version, method = struct.unpack('!BB', buff[:2]) + if version != 0x05 or method == 0xff: + self.pollend() + self.state = 3 + gajim.idlequeue.plug_idle(self, True, False) + + elif self.state == 4: # get approve of our request + if buff == None: + return None + sub_buff = buff[:4] + if len(sub_buff) < 4: + return None + version, command, rsvd, address_type = struct.unpack('!BBBB', buff[:4]) + addrlen, address, port = 0, 0, 0 + if address_type == 0x03: + addrlen = ord(buff[4]) + address = struct.unpack('!%ds' % addrlen, buff[5:addrlen + 5]) + portlen = len(buff[addrlen + 5:]) + if portlen == 1: + port, = struct.unpack('!B', buff[addrlen + 5]) + elif portlen > 2: + port, = struct.unpack('!H', buff[addrlen + 5:]) + self.disconnect() + self.on_success() + + + def do_connect(self): + try: + self._sock.connect((self.host, self.port)) + self._sock.setblocking(False) + self._send=self._sock.send + self._recv=self._sock.recv + except Exception, ee: + (errnum, errstr) = ee + if errnum == 111: + self.on_failure() + return None + # win32 needs this + elif errnum != 10056 or self.state != 0: + return None + else: # socket is already connected + self._sock.setblocking(False) + self._send=self._sock.send + self._recv=self._sock.recv + self.buff = '' + self.state = 1 # connected + self.idlequeue.plug_idle(self, True, False) + return + diff --git a/src/gajim.py b/src/gajim.py index efe31f055..e5dbc2c03 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -108,6 +108,7 @@ import common.sleepy from common.xmpp import idlequeue from common import nslookup +from common import proxy65_manager from common import socks5 from common import gajim from common import connection @@ -1641,6 +1642,7 @@ class Interface: gajim.socks5queue = socks5.SocksQueue(gajim.idlequeue, self.handle_event_file_rcv_completed, self.handle_event_file_progress) + gajim.proxy65_manager = proxy65_manager.Proxy65Manager(gajim.idlequeue) self.register_handlers() for account in gajim.config.get_per('accounts'): gajim.connections[account] = common.connection.Connection(account)