- renamed src/common/nslookup.py to resolver.py

- refactored resolver code and added asynchronous resolver based on patch by Damien Thebault[1]
  * Uses libasyncns-python[2]. If it's not available, old nslookup resolver is used)
  * works for SRV requests only at the moment

[1] https://www.lagaule.org/pipermail/gajim-devel/2008-July/000460.html
[2] https://code.launchpad.net/libasyncns-python
This commit is contained in:
tomk 2008-08-31 23:40:06 +00:00
parent ed7dd84cfe
commit a7c36048b9
10 changed files with 517 additions and 105 deletions

View File

@ -1,4 +1,4 @@
## common/nslookup.py
## common/resolver.py
##
## Copyright (C) 2006 Dimitur Kirov <dkirov@gmail.com>
##
@ -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):
@ -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

View File

@ -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

View File

@ -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:

50
test/lib/__init__.py Normal file
View File

@ -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:

77
test/lib/data.py Executable file
View File

@ -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'<Cool"chârßéµö', u'GroupB'],
'name': None, 'resources': {}, 'subscription': u'both'},
u'samejid@gajim.org': {
'ask': None, 'groups': [u'GroupA',], 'name': None, 'resources': {},
'subscription': u'both'}
}
contacts[account2] = {
u'myjid@'+account2: {
'ask': None, 'groups': [], 'name': None, 'resources': {},
'subscription': u'both'},
u'default3@gajim.org': {
'ask': None, 'groups': [u'GroupC',], 'name': None, 'resources': {},
'subscription': u'both'},
u'asksubfrom@gajim.org': {
'ask': u'subscribe', 'groups': [u'GroupA',], 'name': None,
'resources': {}, 'subscription': u'from'},
u'subto@gajim.org': {
'ask': None, 'groups': [u'GroupB'], 'name': None, 'resources': {},
'subscription': u'to'},
u'samejid@gajim.org': {
'ask': None, 'groups': [u'GroupA', u'GroupB'], 'name': None,
'resources': {}, 'subscription': u'both'}
}
contacts[account3] = {
#u'guypsych0\\40h.com@msn.dingdong.org': {
# 'ask': None, 'groups': [], 'name': None, 'resources': {},
# 'subscription': u'both'},
u'guypsych0%h.com@msn.delx.cjb.net': {
'ask': u'subscribe', 'groups': [], 'name': None,
'resources': {}, 'subscription': u'from'},
#u'guypsych0%h.com@msn.jabber.wiretrip.org': {
# 'ask': None, 'groups': [], 'name': None, 'resources': {},
# 'subscription': u'to'},
#u'guypsycho\\40g.com@gtalk.dingdong.org': {
# 'ask': None, 'groups': [], 'name': None,
# 'resources': {}, 'subscription': u'both'}
}
# We have contacts that are not in roster but only specified in the metadata
metacontact_data = [
[{'account': account3,
'jid': u'guypsych0\\40h.com@msn.dingdong.org',
'order': 0},
{'account': account3,
'jid': u'guypsych0%h.com@msn.delx.cjb.net',
'order': 0},
{'account': account3,
'jid': u'guypsych0%h.com@msn.jabber.wiretrip.org',
'order': 0},
{'account': account3,
'jid': u'guypsycho\\40g.com@gtalk.dingdong.org',
'order': 0}],
[{'account': account1,
'jid': u'samejid@gajim.org',
'order': 0},
{'account': account2,
'jid': u'samejid@gajim.org',
'order': 0}]
]

View File

@ -463,3 +463,5 @@ CALLABLE = callable
# vim: se ts=3:

145
test/lib/mocks.py Normal file
View File

@ -0,0 +1,145 @@
# gajim-specific mock objects
from mock import Mock
from common import gajim
from common.connection_handlers import ConnectionHandlersBase
class MockConnection(Mock, ConnectionHandlersBase):
def __init__(self, account, *args):
Mock.__init__(self, *args)
ConnectionHandlersBase.__init__(self)
self.name = account
self.connected = 2
self.mood = {}
self.activity = {}
self.tune = {}
self.blocked_contacts = {}
self.blocked_groups = {}
self.sessions = {}
gajim.interface.instances[account] = {'infos': {}, 'disco': {},
'gc_config': {}, 'search': {}}
gajim.interface.minimized_controls[account] = {}
gajim.contacts.add_account(account)
gajim.groups[account] = {}
gajim.gc_connected[account] = {}
gajim.automatic_rooms[account] = {}
gajim.newly_added[account] = []
gajim.to_be_removed[account] = []
gajim.nicks[account] = gajim.config.get_per('accounts', account, 'name')
gajim.block_signed_in_notifications[account] = True
gajim.sleeper_state[account] = 0
gajim.encrypted_chats[account] = []
gajim.last_message_time[account] = {}
gajim.status_before_autoaway[account] = ''
gajim.transport_avatar[account] = {}
gajim.gajim_optional_features[account] = []
gajim.caps_hash[account] = ''
gajim.connections[account] = self
class MockWindow(Mock):
def __init__(self, *args):
Mock.__init__(self, *args)
self.window = Mock()
self._controls = {}
def get_control(self, jid, account):
try:
return self._controls[account][jid]
except KeyError:
return None
def has_control(self, jid, acct):
return self.get_control(jid, acct) is not None
def new_tab(self, ctrl):
account = ctrl.account
jid = ctrl.jid
if account not in self._controls:
self._controls[account] = {}
if jid not in self._controls[account]:
self._controls[account][jid] = {}
self._controls[account][jid] = ctrl
def __nonzero__(self):
return True
class MockChatControl(Mock):
def __init__(self, jid, account, *args):
Mock.__init__(self, *args)
self.jid = jid
self.account = account
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, *args):
Mock.__init__(self, *args)
gajim.interface = self
self.msg_win_mgr = Mock()
self.roster = Mock()
self.remote_ctrl = None
self.instances = {}
self.minimized_controls = {}
self.status_sent_to_users = Mock()
if gajim.use_x:
self.jabber_state_images = {'16': {}, '32': {}, 'opened': {},
'closed': {}}
import gtkgui_helpers
gtkgui_helpers.make_jabber_state_images()
else:
self.jabber_state_images = {'16': Mock(), '32': Mock(),
'opened': Mock(), 'closed': Mock()}
class MockLogger(Mock):
def __init__(self):
Mock.__init__(self, {'write': None, 'get_transports_type': {}})
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):
return '<MockSession %s>' % self.thread_id
def __nonzero__(self):
return True
def __eq__(self, other):
return self is other
# vim: se ts=3:

View File

@ -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:

View File

@ -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 '<MockSession %s>' % self.thread_id
def __nonzero__(self):
return True

95
test/test_resolver.py Normal file
View File

@ -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()