diff --git a/src/common/nslookup.py b/src/common/resolver.py similarity index 68% rename from src/common/nslookup.py rename to src/common/resolver.py index ae535993e..52c297ff7 100644 --- a/src/common/nslookup.py +++ b/src/common/resolver.py @@ -1,4 +1,4 @@ -## common/nslookup.py +## common/resolver.py ## ## Copyright (C) 2006 Dimitur Kirov ## @@ -23,6 +23,7 @@ import re from xmpp.idlequeue import * +# needed for nslookup if os.name == 'nt': from subprocess import * # python24 only. we ask this for Windows elif os.name == 'posix': @@ -34,13 +35,138 @@ ns_type_pattern = re.compile('^[a-z]+$') # match srv host_name host_pattern = re.compile('^[a-z0-9\-._]*[a-z0-9]\.[a-z]{2,}$') -class Resolver: +USE_LIBASYNCNS = False + +try: + #raise ImportError("Manually disabled libasync") + import libasyncns + USE_LIBASYNCNS = True + log.info("libasyncns-python loaded") +except ImportError: + log.debug("Import of libasyncns-python failed, getaddrinfo will block", exc_info=True) + + # FIXME: Remove these prints before release, replace with a warning dialog. + print >> sys.stderr, "=" * 79 + print >> sys.stderr, "libasyncns-python not installed which means:" + print >> sys.stderr, " - nslookup will be used for SRV and TXT requests" + print >> sys.stderr, " - getaddrinfo will block" + print >> sys.stderr, "libasyncns-python can be found at https://launchpad.net/libasyncns-python" + print >> sys.stderr, "=" * 79 + + +def get_resolver(idlequeue): + if USE_LIBASYNCNS: + return LibAsyncNSResolver() + else: + return NSLookupResolver(idlequeue) + +class CommonResolver(): + def __init__(self): + # dict {"host+type" : list of records} + self.resolved_hosts = {} + # dict {"host+type" : list of callbacks} + self.handlers = {} + + def resolve(self, host, on_ready, type='srv'): + assert(type in ['srv', 'txt']) + if not host: + # empty host, return empty list of srv records + on_ready([]) + return + if self.resolved_hosts.has_key(host+type): + # host is already resolved, return cached values + on_ready(host, self.resolved_hosts[host+type]) + return + if self.handlers.has_key(host+type): + # host is about to be resolved by another connection, + # attach our callback + self.handlers[host+type].append(on_ready) + else: + # host has never been resolved, start now + self.handlers[host+type] = [on_ready] + self.start_resolve(host, type) + + def _on_ready(self, host, type, result_list): + # practically it is impossible to be the opposite, but who knows :) + if not self.resolved_hosts.has_key(host+type): + self.resolved_hosts[host+type] = result_list + if self.handlers.has_key(host+type): + for callback in self.handlers[host+type]: + callback(host, result_list) + del(self.handlers[host+type]) + + def start_resolve(self, host, type): + pass + + +class LibAsyncNSResolver(CommonResolver): + ''' + Asynchronous resolver using libasyncns-python. process() method has to be called + in order to proceed the pending requests. + Based on patch submitted by Damien Thebault. + ''' + def __init__(self): + self.asyncns = libasyncns.Asyncns() + CommonResolver.__init__(self) + + def start_resolve(self, host, type): + type = libasyncns.ns_t_srv + if type == 'txt': type = libasyncns.ns_t_txt + resq = self.asyncns.res_query(host, libasyncns.ns_c_in, type) + resq.userdata = {'host':host, 'type':type} + + # getaddrinfo to be done + #def resolve_name(self, dname, callback): + #resq = self.asyncns.getaddrinfo(dname) + #resq.userdata = {'callback':callback, 'dname':dname} + + def _on_ready(self, host, type, result_list): + if type == libasyncns.ns_t_srv: type = 'srv' + elif type == libasyncns.ns_t_txt: type = 'txt' + + CommonResolver._on_ready(self, host, type, result_list) + + + def process(self): + try: + self.asyncns.wait(False) + resq = self.asyncns.get_next() + except: + return True + if type(resq) == libasyncns.ResQuery: + # TXT or SRV result + while resq is not None: + try: + rl = resq.get_done() + except: + rl = [] + if rl: + for r in rl: + r['prio'] = r['pref'] + self._on_ready( + host = resq.userdata['host'], + type = resq.userdata['type'], + result_list = rl) + try: + resq = self.asyncns.get_next() + except: + resq = None + elif type(resq) == libasyncns.AddrInfoQuery: + # getaddrinfo result (A or AAAA) + rl = resq.get_done() + resq.userdata['callback'](resq.userdata['dname'], rl) + return True + +class NSLookupResolver(CommonResolver): + ''' + Asynchronous DNS resolver calling nslookup. Processing of pending requests + is invoked from idlequeue which is watching file descriptor of pipe of stdout + of nslookup process. + ''' def __init__(self, idlequeue): self.idlequeue = idlequeue - # dict {host : list of srv records} - self.resolved_hosts = {} - # dict {host : list of callbacks} - self.handlers = {} + self.process = False + CommonResolver.__init__(self) def parse_srv_result(self, fqdn, result): ''' parse the output of nslookup command and return list of @@ -133,42 +259,19 @@ class Resolver: 'prio': prio}) return hosts - def _on_ready(self, host, result): + def _on_ready(self, host, type, result): # nslookup finished, parse the result and call the handlers result_list = self.parse_srv_result(host, result) + CommonResolver._on_ready(self, host, type, result_list) - # practically it is impossible to be the opposite, but who knows :) - if not self.resolved_hosts.has_key(host): - self.resolved_hosts[host] = result_list - if self.handlers.has_key(host): - for callback in self.handlers[host]: - callback(host, result_list) - del(self.handlers[host]) - def start_resolve(self, host): + def start_resolve(self, host, type): ''' spawn new nslookup process and start waiting for results ''' - ns = NsLookup(self._on_ready, host) + ns = NsLookup(self._on_ready, host, type) ns.set_idlequeue(self.idlequeue) ns.commandtimeout = 10 ns.start() - def resolve(self, host, on_ready): - if not host: - # empty host, return empty list of srv records - on_ready([]) - return - if self.resolved_hosts.has_key(host): - # host is already resolved, return cached values - on_ready(host, self.resolved_hosts[host]) - return - if self.handlers.has_key(host): - # host is about to be resolved by another connection, - # attach our callback - self.handlers[host].append(on_ready) - else: - # host has never been resolved, start now - self.handlers[host] = [on_ready] - self.start_resolve(host) # TODO: move IdleCommand class in other file, maybe helpers ? class IdleCommand(IdleObject): @@ -268,7 +371,7 @@ class IdleCommand(IdleObject): self._return_result() class NsLookup(IdleCommand): - def __init__(self, on_result, host='_xmpp-client', type = 'srv'): + def __init__(self, on_result, host='_xmpp-client', type='srv'): IdleCommand.__init__(self, on_result) self.commandtimeout = 10 self.host = host.lower() @@ -288,7 +391,7 @@ class NsLookup(IdleCommand): def _return_result(self): if self.result_handler: - self.result_handler(self.host, self.result) + self.result_handler(self.host, self.type, self.result) self.result_handler = None # below lines is on how to use API and assist in testing diff --git a/src/common/xmpp/client_nb.py b/src/common/xmpp/client_nb.py index b7603cc08..088475b90 100644 --- a/src/common/xmpp/client_nb.py +++ b/src/common/xmpp/client_nb.py @@ -40,6 +40,7 @@ class NonBlockingClient: :param domain: domain - for to: attribute (from account info) :param idlequeue: processing idlequeue :param caller: calling object - it has to implement method _event_dispatcher + which is called from dispatcher instance ''' self.Namespace = protocol.NS_CLIENT self.defaultNamespace = self.Namespace diff --git a/src/gajim.py b/src/gajim.py index be9eaab1a..e5f0b3e03 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -260,7 +260,7 @@ import common.sleepy from common.xmpp import idlequeue from common.zeroconf import connection_zeroconf -from common import nslookup +from common import resolver from common import proxy65_manager from common import socks5 from common import helpers @@ -3077,7 +3077,7 @@ class Interface: # gajim.idlequeue.process() each foo miliseconds gajim.idlequeue = GlibIdleQueue() # resolve and keep current record of resolved hosts - gajim.resolver = nslookup.Resolver(gajim.idlequeue) + gajim.resolver = resolver.get_resolver(gajim.idlequeue) gajim.socks5queue = socks5.SocksQueue(gajim.idlequeue, self.handle_event_file_rcv_completed, self.handle_event_file_progress) @@ -3222,6 +3222,11 @@ class Interface: self.last_ftwindow_update = 0 gobject.timeout_add(100, self.autoconnect) + + # when using libasyncns we need to process resolver in regular intervals + if resolver.USE_LIBASYNCNS: + gobject.timeout_add(200, gajim.resolver.process) + if os.name == 'nt': gobject.timeout_add(200, self.process_connections) else: diff --git a/test/lib/__init__.py b/test/lib/__init__.py new file mode 100644 index 000000000..2b99a7e23 --- /dev/null +++ b/test/lib/__init__.py @@ -0,0 +1,50 @@ +import sys +import os.path +import getopt + +use_x = True +shortargs = 'hnv:' +longargs = 'help no-x verbose=' +opts, args = getopt.getopt(sys.argv[1:], shortargs, longargs.split()) +for o, a in opts: + if o in ('-n', '--no-x'): + use_x = False + +gajim_root = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../..') + +# look for modules in the CWD, then gajim/test/lib, then gajim/src, then everywhere else +sys.path.insert(1, gajim_root + '/src') +sys.path.insert(1, gajim_root + '/test/lib') + +# a temporary version of ~/.gajim for testing +configdir = gajim_root + '/test/tmp' + +# define _ for i18n +import __builtin__ +__builtin__._ = lambda x: x + +import os + +def setup_env(): + # wipe config directory + if os.path.isdir(configdir): + import shutil + shutil.rmtree(configdir) + + os.mkdir(configdir) + + import common.configpaths + common.configpaths.gajimpaths.init(configdir) + common.configpaths.gajimpaths.init_profile() + + # for some reason common.gajim needs to be imported before xmpppy? + from common import gajim + + gajim.DATA_DIR = gajim_root + '/data' + gajim.use_x = use_x + + if use_x: + import gtkgui_helpers + gtkgui_helpers.GLADE_DIR = gajim_root + '/data/glade' + +# vim: se ts=3: diff --git a/test/lib/data.py b/test/lib/data.py new file mode 100755 index 000000000..074a7def8 --- /dev/null +++ b/test/lib/data.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +account1 = u'acc1' +account2 = u'Cool"chârßéµö' +account3 = u'dingdong.org' + +contacts = {} +contacts[account1] = { + u'myjid@'+account1: { + 'ask': None, 'groups': [], 'name': None, 'resources': {}, + 'subscription': u'both'}, + u'default1@gajim.org': { + 'ask': None, 'groups': [], 'name': None, 'resources': {}, + 'subscription': u'both'}, + u'default2@gajim.org': { + 'ask': None, 'groups': [u'GroupA',], 'name': None, 'resources': {}, + 'subscription': u'both'}, + u'Cool"chârßéµö@gajim.org': { + 'ask': None, 'groups': [u'' % self.thread_id + + def __nonzero__(self): + return True + + def __eq__(self, other): + return self is other + +# vim: se ts=3: diff --git a/test/notify.py b/test/lib/notify.py similarity index 96% rename from test/notify.py rename to test/lib/notify.py index 2e55e6959..f14100af3 100644 --- a/test/notify.py +++ b/test/lib/notify.py @@ -13,3 +13,5 @@ def get_show_in_roster(event, account, contact, session = None): def get_show_in_systray(event, account, contact, type_ = None): return True + +# vim: se ts=3: \ No newline at end of file diff --git a/test/mocks.py b/test/mocks.py deleted file mode 100644 index 213f79ddf..000000000 --- a/test/mocks.py +++ /dev/null @@ -1,68 +0,0 @@ -# gajim-specific mock objects -from mock import Mock - -from common import gajim - -class MockConnection(Mock): - def __init__(self, name, *args): - Mock.__init__(self, *args) - self.name = name - gajim.connections[name] = self - -class MockWindow(Mock): - def __init__(self, *args): - Mock.__init__(self, *args) - self.window = Mock() - -class MockChatControl(Mock): - def __init__(self, *args): - Mock.__init__(self, *args) - - self.parent_win = MockWindow({'get_active_control': self}) - self.session = None - - def set_session(self, sess): - self.session = sess - - def __nonzero__(self): - return True - - def __eq__(self, other): - return self is other - -class MockInterface(Mock): - def __init__(self, acct, *args): - Mock.__init__(self, *args) - self.msg_win_mgr = Mock() - self.roster = Mock() - - self.remote_ctrl = None - self.minimized_controls = { acct: {} } - -class MockLogger(Mock): - def __init__(self): - Mock.__init__(self, {'write': None}) - -class MockContact(Mock): - def __nonzero__(self): - return True - -import random - -class MockSession(Mock): - def __init__(self, conn, jid, thread_id, type): - Mock.__init__(self) - - self.conn = conn - self.jid = jid - self.type = type - self.thread_id = thread_id - - if not self.thread_id: - self.thread_id = '%0x' % random.randint(0, 10000) - - def __repr__(self): - print '' % self.thread_id - - def __nonzero__(self): - return True diff --git a/test/test_resolver.py b/test/test_resolver.py new file mode 100644 index 000000000..4cab122a0 --- /dev/null +++ b/test/test_resolver.py @@ -0,0 +1,95 @@ +import unittest + +import time + +import lib +lib.setup_env() + +from common import resolver +from gajim import GlibIdleQueue + +from mock import Mock, expectParams +from mocks import * + +import gtk + + +GMAIL_SRV_NAME = '_xmpp-client._tcp.gmail.com' +NONSENSE_NAME = 'sfsdfsdfsdf.sdfs.fsd' +JABBERCZ_TXT_NAME = '_xmppconnect.jabber.cz' +JABBERCZ_SRV_NAME = '_xmpp-client._tcp.jabber.cz' + +TEST_LIST = [(GMAIL_SRV_NAME, 'srv', True), + (NONSENSE_NAME, 'srv', False), + (JABBERCZ_SRV_NAME, 'srv', True)] + +class TestResolver(unittest.TestCase): + def setUp(self): + self.iq = GlibIdleQueue() + self.reset() + self.resolver = None + + def reset(self): + self.flag = False + self.expect_results = False + self.nslookup = False + self.resolver = None + + def testLibAsyncNSResolver(self): + self.reset() + if not resolver.USE_LIBASYNCNS: + print 'testLibAsyncResolver: libasyncns-python not installed' + return + self.resolver = resolver.LibAsyncNSResolver() + + for name, type, expect_results in TEST_LIST: + self.expect_results = expect_results + self.runLANSR(name, type) + self.flag = False + + def runLANSR(self, name, type): + self.resolver.resolve( + host = name, + type = type, + on_ready = self.myonready) + while not self.flag: + time.sleep(1) + self.resolver.process() + + + def myonready(self, name, result_set): + print 'on_ready called ...' + print 'hostname: %s' % name + print 'result set: %s' % result_set + print 'res.resolved_hosts: %s' % self.resolver.resolved_hosts + if self.expect_results: + self.assert_(len(result_set) > 0) + else: + self.assert_(result_set == []) + self.flag = True + if self.nslookup: self._testNSLR() + + + def testNSLookupResolver(self): + self.reset() + self.nslookup = True + self.resolver = resolver.NSLookupResolver(self.iq) + self.test_list = TEST_LIST + self._testNSLR() + try: + gtk.main() + except KeyboardInterrupt: + print 'KeyboardInterrupt caught' + + def _testNSLR(self): + if self.test_list == []: + gtk.main_quit() + return + name, type, self.expect_results = self.test_list.pop() + self.resolver.resolve( + host = name, + type = type, + on_ready = self.myonready) + +if __name__ == '__main__': + unittest.main()