Commit 88504d12 authored by Jelte Jansen's avatar Jelte Jansen
Browse files

Merge branch 'trac811_new'

parents a251c4f2 4bd053d4
# Copyright (C) 2009 Internet Systems Consortium.
# Copyright (C) 2009-2011 Internet Systems Consortium.
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
......@@ -20,8 +20,10 @@ from xfrin import *
#
# Commonly used (mostly constant) test parameters
#
TEST_ZONE_NAME = "example.com"
TEST_ZONE_NAME_STR = "example.com."
TEST_ZONE_NAME = Name(TEST_ZONE_NAME_STR)
TEST_RRCLASS = RRClass.IN()
TEST_RRCLASS_STR = 'IN'
TEST_DB_FILE = 'db_file'
TEST_MASTER_IPV4_ADDRESS = '127.0.0.1'
TEST_MASTER_IPV4_ADDRINFO = (socket.AF_INET, socket.SOCK_STREAM,
......@@ -40,12 +42,12 @@ TSIG_KEY = TSIGKey("example.com:SFuWd/q99SzF8Yzd1QbB9g==")
soa_rdata = Rdata(RRType.SOA(), TEST_RRCLASS,
'master.example.com. admin.example.com ' +
'1234 3600 1800 2419200 7200')
soa_rrset = RRset(Name(TEST_ZONE_NAME), TEST_RRCLASS, RRType.SOA(),
soa_rrset = RRset(TEST_ZONE_NAME, TEST_RRCLASS, RRType.SOA(),
RRTTL(3600))
soa_rrset.add_rdata(soa_rdata)
example_axfr_question = Question(Name(TEST_ZONE_NAME), TEST_RRCLASS,
example_axfr_question = Question(TEST_ZONE_NAME, TEST_RRCLASS,
RRType.AXFR())
example_soa_question = Question(Name(TEST_ZONE_NAME), TEST_RRCLASS,
example_soa_question = Question(TEST_ZONE_NAME, TEST_RRCLASS,
RRType.SOA())
default_questions = [example_axfr_question]
default_answers = [soa_rrset]
......@@ -60,6 +62,13 @@ def strip_mutable_tsig_data(data):
# Time Signed.
return data[0:-32] + data[-26:-22] + data[-6:]
class MockCC():
def get_default_value(self, identifier):
if identifier == "zones/master_port":
return TEST_MASTER_PORT
if identifier == "zones/class":
return TEST_RRCLASS_STR
class MockXfrin(Xfrin):
# This is a class attribute of a callable object that specifies a non
# default behavior triggered in _cc_check_command(). Specific test methods
......@@ -69,7 +78,8 @@ class MockXfrin(Xfrin):
check_command_hook = None
def _cc_setup(self):
self._tsig_key_str = None
self._tsig_key = None
self._module_cc = MockCC()
pass
def _get_db_file(self):
......@@ -80,6 +90,16 @@ class MockXfrin(Xfrin):
if MockXfrin.check_command_hook:
MockXfrin.check_command_hook()
def xfrin_start(self, zone_name, rrclass, db_file, master_addrinfo,
tsig_key, check_soa=True):
# store some of the arguments for verification, then call this
# method in the superclass
self.xfrin_started_master_addr = master_addrinfo[2][0]
self.xfrin_started_master_port = master_addrinfo[2][1]
return Xfrin.xfrin_start(self, zone_name, rrclass, db_file,
master_addrinfo, tsig_key,
check_soa)
class MockXfrinConnection(XfrinConnection):
def __init__(self, sock_map, zone_name, rrclass, db_file, shutdown_event,
master_addr):
......@@ -450,7 +470,8 @@ class TestXfrin(unittest.TestCase):
sys.stderr = open(os.devnull, 'w')
self.xfr = MockXfrin()
self.args = {}
self.args['zone_name'] = TEST_ZONE_NAME
self.args['zone_name'] = TEST_ZONE_NAME_STR
self.args['class'] = TEST_RRCLASS_STR
self.args['port'] = TEST_MASTER_PORT
self.args['master'] = TEST_MASTER_IPV4_ADDRESS
self.args['db_file'] = TEST_DB_FILE
......@@ -464,7 +485,8 @@ class TestXfrin(unittest.TestCase):
return self.xfr._parse_zone_name_and_class(self.args)
def _do_parse_master_port(self):
return self.xfr._parse_master_and_port(self.args)
name, rrclass = self._do_parse_zone_name_class()
return self.xfr._parse_master_and_port(self.args, name, rrclass)
def test_parse_cmd_params(self):
name, rrclass = self._do_parse_zone_name_class()
......@@ -492,7 +514,7 @@ class TestXfrin(unittest.TestCase):
def test_parse_cmd_params_bogusclass(self):
self.args['zone_class'] = 'XXX'
self.assertRaises(XfrinException, self._do_parse_zone_name_class)
self.assertRaises(XfrinZoneInfoException, self._do_parse_zone_name_class)
def test_parse_cmd_params_nozone(self):
# zone name is mandatory.
......@@ -502,8 +524,7 @@ class TestXfrin(unittest.TestCase):
def test_parse_cmd_params_nomaster(self):
# master address is mandatory.
del self.args['master']
master_addrinfo = self._do_parse_master_port()
self.assertEqual(master_addrinfo[2][0], DEFAULT_MASTER)
self.assertRaises(XfrinException, self._do_parse_master_port)
def test_parse_cmd_params_bad_ip4(self):
self.args['master'] = '3.3.3.3.3'
......@@ -533,6 +554,77 @@ class TestXfrin(unittest.TestCase):
def test_command_handler_retransfer(self):
self.assertEqual(self.xfr.command_handler("retransfer",
self.args)['result'][0], 0)
self.assertEqual(self.args['master'], self.xfr.xfrin_started_master_addr)
self.assertEqual(int(self.args['port']), self.xfr.xfrin_started_master_port)
def test_command_handler_retransfer_short_command1(self):
# try it when only specifying the zone name (of unknown zone)
# this should fail because master address is not specified.
short_args = {}
short_args['zone_name'] = TEST_ZONE_NAME_STR
self.assertEqual(self.xfr.command_handler("retransfer",
short_args)['result'][0], 1)
def test_command_handler_retransfer_short_command2(self):
# try it when only specifying the zone name (of known zone)
short_args = {}
short_args['zone_name'] = TEST_ZONE_NAME_STR
zones = { 'zones': [
{ 'name': TEST_ZONE_NAME_STR,
'master_addr': TEST_MASTER_IPV4_ADDRESS,
'master_port': TEST_MASTER_PORT
}
]}
self.xfr.config_handler(zones)
self.assertEqual(self.xfr.command_handler("retransfer",
short_args)['result'][0], 0)
self.assertEqual(TEST_MASTER_IPV4_ADDRESS,
self.xfr.xfrin_started_master_addr)
self.assertEqual(int(TEST_MASTER_PORT),
self.xfr.xfrin_started_master_port)
def test_command_handler_retransfer_short_command3(self):
# try it when only specifying the zone name (of known zone)
short_args = {}
# test it without the trailing root dot
short_args['zone_name'] = TEST_ZONE_NAME_STR[:-1]
zones = { 'zones': [
{ 'name': TEST_ZONE_NAME_STR,
'master_addr': TEST_MASTER_IPV4_ADDRESS,
'master_port': TEST_MASTER_PORT
}
]}
self.xfr.config_handler(zones)
self.assertEqual(self.xfr.command_handler("retransfer",
short_args)['result'][0], 0)
self.assertEqual(TEST_MASTER_IPV4_ADDRESS,
self.xfr.xfrin_started_master_addr)
self.assertEqual(int(TEST_MASTER_PORT),
self.xfr.xfrin_started_master_port)
def test_command_handler_retransfer_short_command4(self):
# try it when only specifying the zone name (of known zone, with
# different case)
short_args = {}
# swap the case of the zone name in our command
short_args['zone_name'] = TEST_ZONE_NAME_STR.swapcase()
zones = { 'zones': [
{ 'name': TEST_ZONE_NAME_STR,
'master_addr': TEST_MASTER_IPV4_ADDRESS,
'master_port': TEST_MASTER_PORT
}
]}
self.xfr.config_handler(zones)
self.assertEqual(self.xfr.command_handler("retransfer",
short_args)['result'][0], 0)
self.assertEqual(TEST_MASTER_IPV4_ADDRESS,
self.xfr.xfrin_started_master_addr)
self.assertEqual(int(TEST_MASTER_PORT),
self.xfr.xfrin_started_master_port)
def test_command_handler_retransfer_badcommand(self):
self.args['master'] = 'invalid'
......@@ -540,13 +632,15 @@ class TestXfrin(unittest.TestCase):
self.args)['result'][0], 1)
def test_command_handler_retransfer_quota(self):
self.args['master'] = TEST_MASTER_IPV4_ADDRESS
for i in range(self.xfr._max_transfers_in - 1):
self.xfr.recorder.increment(str(i) + TEST_ZONE_NAME)
self.xfr.recorder.increment(Name(str(i) + TEST_ZONE_NAME_STR))
# there can be one more outstanding transfer.
self.assertEqual(self.xfr.command_handler("retransfer",
self.args)['result'][0], 0)
# make sure the # xfrs would excceed the quota
self.xfr.recorder.increment(str(self.xfr._max_transfers_in) + TEST_ZONE_NAME)
self.xfr.recorder.increment(Name(str(self.xfr._max_transfers_in) + TEST_ZONE_NAME_STR))
# this one should fail
self.assertEqual(self.xfr.command_handler("retransfer",
self.args)['result'][0], 1)
......@@ -570,14 +664,43 @@ class TestXfrin(unittest.TestCase):
self.args['master'] = TEST_MASTER_IPV6_ADDRESS
self.assertEqual(self.xfr.command_handler("refresh",
self.args)['result'][0], 0)
self.assertEqual(TEST_MASTER_IPV6_ADDRESS,
self.xfr.xfrin_started_master_addr)
self.assertEqual(int(TEST_MASTER_PORT),
self.xfr.xfrin_started_master_port)
def test_command_handler_notify(self):
# at this level, refresh is no different than retransfer.
self.args['master'] = TEST_MASTER_IPV6_ADDRESS
# ...but right now we disable the feature due to security concerns.
# ...but the zone is unknown so this would return an error
self.assertEqual(self.xfr.command_handler("notify",
self.args)['result'][0], 1)
def test_command_handler_notify_known_zone(self):
# try it with a known zone
self.args['master'] = TEST_MASTER_IPV6_ADDRESS
# but use a different address in the actual command
zones = { 'zones': [
{ 'name': TEST_ZONE_NAME_STR,
'master_addr': TEST_MASTER_IPV4_ADDRESS,
'master_port': TEST_MASTER_PORT
}
]}
self.xfr.config_handler(zones)
self.assertEqual(self.xfr.command_handler("notify",
self.args)['result'][0], 0)
# and see if we used the address from the command, and not from
# the config
# This is actually NOT the address given in the command, which
# would at this point not make sense, see the TODO in
# xfrin.py.in Xfrin.command_handler())
self.assertEqual(TEST_MASTER_IPV4_ADDRESS,
self.xfr.xfrin_started_master_addr)
self.assertEqual(int(TEST_MASTER_PORT),
self.xfr.xfrin_started_master_port)
def test_command_handler_unknown(self):
self.assertEqual(self.xfr.command_handler("xxx", None)['result'][0], 1)
......@@ -586,20 +709,145 @@ class TestXfrin(unittest.TestCase):
self.assertEqual(self.xfr.config_handler({'transfers_in': 3})['result'][0], 0)
self.assertEqual(self.xfr._max_transfers_in, 3)
def test_command_handler_masters(self):
master_info = {'master_addr': '1.1.1.1', 'master_port':53}
self.assertEqual(self.xfr.config_handler(master_info)['result'][0], 0)
master_info = {'master_addr': '1111.1.1.1', 'master_port':53 }
self.assertEqual(self.xfr.config_handler(master_info)['result'][0], 1)
master_info = {'master_addr': '2.2.2.2', 'master_port':530000 }
self.assertEqual(self.xfr.config_handler(master_info)['result'][0], 1)
master_info = {'master_addr': '2.2.2.2', 'master_port':53 }
self.xfr.config_handler(master_info)
self.assertEqual(self.xfr._master_addr, '2.2.2.2')
self.assertEqual(self.xfr._master_port, 53)
def _check_zones_config(self, config_given):
if 'transfers_in' in config_given:
self.assertEqual(config_given['transfers_in'],
self.xfr._max_transfers_in)
for zone_config in config_given['zones']:
zone_name = zone_config['name']
zone_info = self.xfr._get_zone_info(Name(zone_name), RRClass.IN())
self.assertEqual(str(zone_info.master_addr), zone_config['master_addr'])
self.assertEqual(zone_info.master_port, zone_config['master_port'])
if 'tsig_key' in zone_config:
self.assertEqual(zone_info.tsig_key.to_text(), TSIGKey(zone_config['tsig_key']).to_text())
else:
self.assertIsNone(zone_info.tsig_key)
def test_command_handler_zones(self):
config1 = { 'transfers_in': 3,
'zones': [
{ 'name': 'test.example.',
'master_addr': '192.0.2.1',
'master_port': 53
}
]}
self.assertEqual(self.xfr.config_handler(config1)['result'][0], 0)
self._check_zones_config(config1)
config2 = { 'transfers_in': 4,
'zones': [
{ 'name': 'test.example.',
'master_addr': '192.0.2.2',
'master_port': 53,
'tsig_key': "example.com:SFuWd/q99SzF8Yzd1QbB9g=="
}
]}
self.assertEqual(self.xfr.config_handler(config2)['result'][0], 0)
self._check_zones_config(config2)
# test that configuring the zone multiple times fails
zones = { 'transfers_in': 5,
'zones': [
{ 'name': 'test.example.',
'master_addr': '192.0.2.1',
'master_port': 53
},
{ 'name': 'test.example.',
'master_addr': '192.0.2.2',
'master_port': 53
}
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
# since this has failed, we should still have the previous config
self._check_zones_config(config2)
zones = { 'zones': [
{ 'name': 'test.example.',
'master_addr': '192.0.2.3',
'master_port': 53,
'class': 'BADCLASS'
}
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
self._check_zones_config(config2)
zones = { 'zones': [
{ 'master_addr': '192.0.2.4',
'master_port': 53
}
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
# since this has failed, we should still have the previous config
self._check_zones_config(config2)
zones = { 'zones': [
{ 'name': 'bad..zone.',
'master_addr': '192.0.2.5',
'master_port': 53
}
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
# since this has failed, we should still have the previous config
self._check_zones_config(config2)
zones = { 'zones': [
{ 'name': '',
'master_addr': '192.0.2.6',
'master_port': 53
}
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
# since this has failed, we should still have the previous config
self._check_zones_config(config2)
zones = { 'zones': [
{ 'name': 'test.example',
'master_addr': 'badaddress',
'master_port': 53
}
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
# since this has failed, we should still have the previous config
self._check_zones_config(config2)
zones = { 'zones': [
{ 'name': 'test.example',
'master_addr': '192.0.2.7',
'master_port': 'bad_port'
}
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
# since this has failed, we should still have the previous config
self._check_zones_config(config2)
zones = { 'zones': [
{ 'name': 'test.example',
'master_addr': '192.0.2.7',
'master_port': 53,
# using a bad TSIG key spec
'tsig_key': "bad..example.com:SFuWd/q99SzF8Yzd1QbB9g=="
}
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
# since this has failed, we should still have the previous config
self._check_zones_config(config2)
# let's also add a zone that is correct too, and make sure
# that the new config is not partially taken
zones = { 'zones': [
{ 'name': 'test.example.',
'master_addr': '192.0.2.8',
'master_port': 53
},
{ 'name': 'test2.example.',
'master_addr': '192.0.2.9',
'master_port': 53,
'tsig_key': 'badkey'
}
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
# since this has failed, we should still have the previous config
self._check_zones_config(config2)
def raise_interrupt():
......
#!@PYTHON@
# Copyright (C) 2010 Internet Systems Consortium.
# Copyright (C) 2009-2011 Internet Systems Consortium.
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
......@@ -56,26 +56,66 @@ XFROUT_MODULE_NAME = 'Xfrout'
ZONE_MANAGER_MODULE_NAME = 'Zonemgr'
REFRESH_FROM_ZONEMGR = 'refresh_from_zonemgr'
ZONE_XFRIN_FAILED = 'zone_xfrin_failed'
# These two default are currently hard-coded. For config this isn't
# necessary, but we need these defaults for optional command arguments
# (TODO: have similar support to get default values for command
# arguments as we do for config options)
DEFAULT_MASTER_PORT = 53
DEFAULT_ZONE_CLASS = RRClass.IN()
__version__ = 'BIND10'
# define xfrin rcode
XFRIN_OK = 0
XFRIN_FAIL = 1
DEFAULT_MASTER_PORT = '53'
DEFAULT_MASTER = '127.0.0.1'
def log_error(msg):
sys.stderr.write("[b10-xfrin] %s\n" % str(msg))
class XfrinException(Exception):
pass
class XfrinZoneInfoException(Exception):
"""This exception is raised if there is an error in the given
configuration (part), or when a command does not have a required
argument or has bad arguments, for instance when the zone's master
address is not a valid IP address, when the zone does not
have a name, or when multiple settings are given for the same
zone."""
pass
def _check_zone_name(zone_name_str):
"""Checks if the given zone name is a valid domain name, and returns
it as a Name object. Raises an XfrinException if it is not."""
try:
# In the _zones dict, part of the key is the zone name,
# but due to a limitation in the Name class, we
# cannot directly use it as a dict key, and we use to_text()
#
# Downcase the name here for that reason.
return Name(zone_name_str, True)
except (EmptyLabel, TooLongLabel, BadLabelType, BadEscape,
TooLongName, IncompleteName) as ne:
raise XfrinZoneInfoException("bad zone name: " + zone_name_str + " (" + str(ne) + ")")
def _check_zone_class(zone_class_str):
"""If the given argument is a string: checks if the given class is
a valid one, and returns an RRClass object if so.
Raises XfrinZoneInfoException if not.
If it is None, this function returns the default RRClass.IN()"""
if zone_class_str is None:
return DEFAULT_ZONE_CLASS
try:
return RRClass(zone_class_str)
except InvalidRRClass as irce:
raise XfrinZoneInfoException("bad zone class: " + zone_class_str + " (" + str(irce) + ")")
class XfrinConnection(asyncore.dispatcher):
'''Do xfrin in this class. '''
def __init__(self,
sock_map, zone_name, rrclass, db_file, shutdown_event,
master_addrinfo, tsig_key_str = None, verbose = False,
master_addrinfo, tsig_key = None, verbose = False,
idle_timeout = 60):
''' idle_timeout: max idle time for read data from socket.
db_file: specify the data source file.
......@@ -95,8 +135,8 @@ class XfrinConnection(asyncore.dispatcher):
self._verbose = verbose
self._master_address = master_addrinfo[2]
self._tsig_ctx = None
if tsig_key_str is not None:
self._tsig_ctx = TSIGContext(TSIGKey(tsig_key_str))
if tsig_key is not None:
self._tsig_ctx = TSIGContext(tsig_key)
def connect_to_master(self):
'''Connect to master in TCP.'''
......@@ -333,12 +373,12 @@ class XfrinConnection(asyncore.dispatcher):
def process_xfrin(server, xfrin_recorder, zone_name, rrclass, db_file,
shutdown_event, master_addrinfo, check_soa, verbose,
tsig_key_str):
tsig_key):
xfrin_recorder.increment(zone_name)
sock_map = {}
conn = XfrinConnection(sock_map, zone_name, rrclass, db_file,
shutdown_event, master_addrinfo,
tsig_key_str, verbose)
tsig_key, verbose)
ret = XFRIN_FAIL
if conn.connect_to_master():
ret = conn.do_xfrin(check_soa)
......@@ -378,12 +418,100 @@ class XfrinRecorder:
self._lock.release()
return ret
class ZoneInfo:
def __init__(self, config_data, module_cc):
"""Creates a zone_info with the config data element as
specified by the 'zones' list in xfrin.spec. Module_cc is
needed to get the defaults from the specification"""
self._module_cc = module_cc
self.set_name(config_data.get('name'))
self.set_master_addr(config_data.get('master_addr'))
self.set_master_port(config_data.get('master_port'))
self.set_zone_class(config_data.get('class'))
self.set_tsig_key(config_data.get('tsig_key'))
def set_name(self, name_str):
"""Set the name for this zone given a name string.
Raises XfrinZoneInfoException if name_str is None or if it
cannot be parsed."""
if name_str is None:
raise XfrinZoneInfoException("Configuration zones list "
"element does not contain "
"'name' attribute")
else:
self.name = _check_zone_name(name_str)
def set_master_addr(self, master_addr_str):
"""Set the master address for this zone given an IP address
string. Raises XfrinZoneInfoException if master_addr_str is
None or if it cannot be parsed."""
if master_addr_str is None:
raise XfrinZoneInfoException("master address missing from config data")
else:
try:
self.master_addr = isc.net.parse.addr_parse(master_addr_str)
except ValueError:
errmsg = "bad format for zone's master: " + master_addr_str
log_error(errmsg)
raise XfrinZoneInfoException(errmsg)
def set_master_port(self, master_port_str):
"""Set the master port given a port number string. If
master_port_str is None, the default from the specification
for this module will be used. Raises XfrinZoneInfoException if
the string contains an invalid port number"""
if master_port_str is None:
self.master_port = self._module_cc.get_default_value("zones/master_port")
else:
try:
self.master_port = isc.net.parse.port_parse(master_port_str)
except ValueError:
errmsg = "bad format for zone's master port: " + master_port_str
log_error(errmsg)
raise XfrinZoneInfoException(errmsg)
def set_zone_class(self, zone_class_str):
"""Set the zone class given an RR class str (e.g. "IN"). If
zone_class_str is None, it will default to what is specified
in the specification file for this module. Raises
XfrinZoneInfoException if the string cannot be parsed."""
# TODO: remove _str
self.class_str = zone_class_str or self._module_cc.get_default_value("zones/class")
if zone_class_str == None:
#TODO rrclass->zone_class
self.rrclass = RRClass(self._module_cc.get_default_value("zones/class"))
else:
try:
self.rrclass = RRClass(zone_class_str)
except InvalidRRClass:
errmsg = "invalid zone class: " + zone_class_str
log_error(errmsg)
raise XfrinZoneInfoException(errmsg)
def set_tsig_key(self, tsig_key_str):
"""Set the tsig_key for this zone, given a TSIG key string
representation. If tsig_key_str is None, no TSIG key will
be set. Raises XfrinZoneInfoException if tsig_key_str cannot
be parsed."""
if tsig_key_str is None:
self.tsig_key = None
else:
try:
self.tsig_key = TSIGKey(tsig_key_str)
except InvalidParameter as ipe:
errmsg = "bad TSIG key string: " + tsig_key_str
log_error(errmsg)
raise XfrinZoneInfoException(errmsg)
def get_master_addr_info(self):
return (self.master_addr.family, socket.SOCK_STREAM,
(str(self.master_addr), self.master_port))
class Xfrin:
def __init__(self, verbose = False):
self._max_transfers_in = 10
#TODO, this is the temp way to set the zone's master.
self._master_addr = DEFAULT_MASTER
self._master_port = DEFAULT_MASTER_PORT
self._zones = {}
self._cc_setup()
self.recorder = XfrinRecorder()
self._shutdown_event = threading.Event()
......@@ -402,10 +530,7 @@ class Xfrin:
self.command_handler)
self._module_cc.start()
config_data = self._module_cc.get_full_config()
self._max_transfers_in = config_data.get("transfers_in")
self._master_addr = config_data.get('master_addr') or self._master_addr