ddns_test.py 58 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335
  1. # Copyright (C) 2011 Internet Systems Consortium.
  2. #
  3. # Permission to use, copy, modify, and distribute this software for any
  4. # purpose with or without fee is hereby granted, provided that the above
  5. # copyright notice and this permission notice appear in all copies.
  6. #
  7. # THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
  8. # DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
  9. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
  10. # INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
  11. # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
  12. # FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
  13. # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
  14. # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  15. '''Tests for the DDNS module'''
  16. from isc.ddns.session import *
  17. from isc.dns import *
  18. from isc.acl.acl import ACCEPT
  19. import isc.util.cio.socketsession
  20. from isc.cc.session import SessionTimeout, SessionError, ProtocolError
  21. from isc.datasrc import DataSourceClient
  22. from isc.config import module_spec_from_file
  23. from isc.config.config_data import ConfigData
  24. from isc.config.ccsession import create_answer, ModuleCCSessionError
  25. from isc.config.module_spec import ModuleSpecError
  26. from isc.server_common.dns_tcp import DNSTCPContext
  27. import ddns
  28. import errno
  29. import os
  30. import select
  31. import shutil
  32. import socket
  33. import unittest
  34. # Some common test parameters
  35. TESTDATA_PATH = os.environ['TESTDATA_PATH'] + os.sep
  36. READ_ZONE_DB_FILE = TESTDATA_PATH + "rwtest.sqlite3" # original, to be copied
  37. TEST_ZONE_NAME = Name('example.org')
  38. TEST_ZONE_NAME_STR = TEST_ZONE_NAME.to_text()
  39. UPDATE_RRTYPE = RRType.SOA()
  40. TEST_QID = 5353 # arbitrary chosen
  41. TEST_RRCLASS = RRClass.IN()
  42. TEST_RRCLASS_STR = TEST_RRCLASS.to_text()
  43. TEST_SERVER6 = ('2001:db8::53', 53, 0, 0)
  44. TEST_CLIENT6 = ('2001:db8::1', 53000, 0, 0)
  45. TEST_SERVER4 = ('192.0.2.53', 53)
  46. TEST_CLIENT4 = ('192.0.2.1', 53534)
  47. TEST_ZONE_RECORD = Question(TEST_ZONE_NAME, TEST_RRCLASS, UPDATE_RRTYPE)
  48. TEST_ACL_CONTEXT = isc.acl.dns.RequestContext(
  49. socket.getaddrinfo("192.0.2.1", 1234, 0, socket.SOCK_DGRAM,
  50. socket.IPPROTO_UDP, socket.AI_NUMERICHOST)[0][4])
  51. # TSIG key for tests when needed. The key name is TEST_ZONE_NAME.
  52. TEST_TSIG_KEY = TSIGKey("example.org:SFuWd/q99SzF8Yzd1QbB9g==")
  53. # TSIG keyring that contanins the test key
  54. TEST_TSIG_KEYRING = TSIGKeyRing()
  55. TEST_TSIG_KEYRING.add(TEST_TSIG_KEY)
  56. # Another TSIG key not in the keyring, making verification fail
  57. BAD_TSIG_KEY = TSIGKey("example.com:SFuWd/q99SzF8Yzd1QbB9g==")
  58. # Incorporate it so we can use the real default values of zonemgr config
  59. # in the tests.
  60. ZONEMGR_MODULE_SPEC = module_spec_from_file(
  61. os.environ["B10_FROM_BUILD"] + "/src/bin/zonemgr/zonemgr.spec")
  62. class FakeSocket:
  63. """
  64. A fake socket. It only provides a file number, peer name and accept method.
  65. """
  66. def __init__(self, fileno, proto=socket.IPPROTO_UDP):
  67. self.proto = proto
  68. self.__fileno = fileno
  69. self._sent_data = None
  70. self._sent_addr = None
  71. self._close_called = 0 # number of calls to close()
  72. self.__send_cc = 0 # waterline of the send buffer (can be reset)
  73. # customizable by tests; if set to True, sendto() will throw after
  74. # recording the parameters.
  75. self._raise_on_send = False
  76. self._send_buflen = None # imaginary send buffer for partial send
  77. def fileno(self):
  78. return self.__fileno
  79. def getpeername(self):
  80. if self.proto == socket.IPPROTO_UDP or \
  81. self.proto == socket.IPPROTO_TCP:
  82. return TEST_CLIENT4
  83. return "fake_unix_socket"
  84. def accept(self):
  85. return FakeSocket(self.__fileno + 1), '/dummy/path'
  86. def sendto(self, data, addr):
  87. self._sent_data = data
  88. self._sent_addr = addr
  89. if self._raise_on_send:
  90. raise socket.error('test socket failure')
  91. def send(self, data):
  92. if self._raise_on_send:
  93. raise socket.error(errno.EPIPE, 'faked connection disruption')
  94. elif self._send_buflen is None:
  95. available_space = len(data)
  96. else:
  97. available_space = self._send_buflen - self.__send_cc
  98. if available_space == 0:
  99. # if there's no space, (assuming it's nonblocking mode) raise
  100. # EAGAIN.
  101. raise socket.error(errno.EAGAIN,
  102. "Resource temporarily unavailable")
  103. # determine the sendable part of the data, record it, update "buffer".
  104. cc = min(available_space, len(data))
  105. if self._sent_data is None:
  106. self._sent_data = data[:cc]
  107. else:
  108. self._sent_data += data[:cc]
  109. self.__send_cc += cc
  110. return cc
  111. def setblocking(self, on):
  112. # We only need a faked NO-OP implementation.
  113. pass
  114. def close(self):
  115. self._close_called += 1
  116. def clear(self):
  117. '''Clear internal instrumental data.'''
  118. self._sent_data = None
  119. self._sent_addr = None
  120. def make_send_ready(self):
  121. # pretend that the accrued data has been cleared, making room in
  122. # the send buffer.
  123. self.__send_cc = 0
  124. class FakeSessionReceiver:
  125. """
  126. A fake socket session receiver, for our tests.
  127. """
  128. def __init__(self, socket):
  129. self._socket = socket
  130. def socket(self):
  131. """
  132. This method is not present in the real receiver, but we use it to
  133. inspect the socket passed to the constructor.
  134. """
  135. return self._socket
  136. class FakeUpdateSession:
  137. '''A fake update session, emulating isc.ddns.session.UpdateSession.
  138. It provides the same interfaces as UpdateSession with skipping complicated
  139. internal protocol processing and returning given faked results. This
  140. will help simplify test setups.
  141. '''
  142. def __init__(self, msg, client_addr, zone_config, faked_result):
  143. '''Faked constructor.
  144. It takes an additional faked_result parameter. It will be used
  145. as the result value of handle(). If its value is UPDATE_ERROR,
  146. get_message() will create a response message whose Rcode is
  147. REFUSED.
  148. '''
  149. self.__msg = msg
  150. self.__faked_result = faked_result
  151. def handle(self):
  152. if self.__faked_result == UPDATE_SUCCESS:
  153. return self.__faked_result, TEST_ZONE_NAME, TEST_RRCLASS
  154. return self.__faked_result, None, None
  155. def get_message(self):
  156. self.__msg.make_response()
  157. self.__msg.clear_section(SECTION_ZONE)
  158. if self.__faked_result == UPDATE_SUCCESS:
  159. self.__msg.set_rcode(Rcode.NOERROR())
  160. else:
  161. self.__msg.set_rcode(Rcode.REFUSED())
  162. return self.__msg
  163. class FakeKeyringModule:
  164. '''Fake the entire isc.server_common.tsig_keyring module.'''
  165. def init_keyring(self, cc):
  166. '''Set the instrumental attribute to True when called.
  167. It can be used for a test that confirms TSIG key initialization is
  168. surely performed. This class doesn't use any CC session, so the
  169. cc parameter will be ignored.
  170. '''
  171. self.initialized = True
  172. def get_keyring(self):
  173. '''Simply return the predefined TSIG keyring unconditionally.'''
  174. return TEST_TSIG_KEYRING
  175. class MyCCSession(isc.config.ConfigData):
  176. '''Fake session with minimal interface compliance.'''
  177. # faked CC sequence used in group_send/recvmsg
  178. FAKE_SEQUENCE = 53
  179. def __init__(self):
  180. module_spec = isc.config.module_spec_from_file(
  181. ddns.SPECFILE_LOCATION)
  182. isc.config.ConfigData.__init__(self, module_spec)
  183. self._started = False
  184. self._stopped = False
  185. # Used as the return value of get_remote_config_value. Customizable.
  186. self.auth_db_file = READ_ZONE_DB_FILE
  187. # Used as the return value of get_remote_config_value. Customizable.
  188. self.auth_datasources = None
  189. # faked cc channel, providing group_send/recvmsg itself. The following
  190. # attributes are for inspection/customization in tests.
  191. self._session = self
  192. self._sent_msg = []
  193. self._recvmsg_called = 0
  194. self._answer_code = 0 # code used in answer returned via recvmsg
  195. self._sendmsg_exception = None # will be raised from sendmsg if !None
  196. self._recvmsg_exception = None # will be raised from recvmsg if !None
  197. # Attributes to handle (faked) remote configurations
  198. self.__callbacks = {} # record callbacks for updates to remote confs
  199. self._raise_mods = {} # map of module to exceptions to be triggered
  200. # on add_remote. settable by tests.
  201. self._auth_config = {} # faked auth cfg, settable by tests
  202. self._zonemgr_config = {} # faked zonemgr cfg, settable by tests
  203. def start(self):
  204. '''Called by DDNSServer initialization, but not used in tests'''
  205. self._started = True
  206. def send_stopping(self):
  207. '''Called by shutdown code'''
  208. self._stopped = True
  209. def get_socket(self):
  210. """
  211. Used to get the file number for select.
  212. """
  213. return FakeSocket(1)
  214. def add_remote_config_by_name(self, module_name, update_callback=None):
  215. # If a list of exceptions is given for the module, raise the front one,
  216. # removing that exception from the list (so the list length controls
  217. # how many (and which) exceptions should be raised on add_remote).
  218. if module_name in self._raise_mods.keys() and \
  219. len(self._raise_mods[module_name]) != 0:
  220. ex = self._raise_mods[module_name][0]
  221. self._raise_mods[module_name] = self._raise_mods[module_name][1:]
  222. raise ex('Failure requesting remote config data')
  223. if update_callback is not None:
  224. self.__callbacks[module_name] = update_callback
  225. if module_name is 'Auth':
  226. if module_name in self.__callbacks:
  227. # ddns implementation doesn't use the 2nd element, so just
  228. # setting it to None
  229. self.__callbacks[module_name](self._auth_config, None)
  230. if module_name is 'Zonemgr':
  231. if module_name in self.__callbacks:
  232. self.__callbacks[module_name](self._zonemgr_config,
  233. ConfigData(ZONEMGR_MODULE_SPEC))
  234. def get_remote_config_value(self, module_name, item):
  235. if module_name == "Auth" and item == "database_file":
  236. return self.auth_db_file, False
  237. if module_name == "Auth" and item == "datasources":
  238. if self.auth_datasources is None:
  239. return [], True # default
  240. else:
  241. return self.auth_datasources, False
  242. if module_name == 'Zonemgr' and item == 'secondary_zones':
  243. if item in self._zonemgr_config:
  244. return self._zonemgr_config[item], False
  245. else:
  246. seczone_default = \
  247. ConfigData(ZONEMGR_MODULE_SPEC).get_default_value(
  248. 'secondary_zones')
  249. return seczone_default, True
  250. def group_sendmsg(self, msg, group):
  251. # remember the passed parameter, and return dummy sequence
  252. self._sent_msg.append((msg, group))
  253. if self._sendmsg_exception is not None:
  254. raise self._sendmsg_exception
  255. return self.FAKE_SEQUENCE
  256. def group_recvmsg(self, nonblock, seq):
  257. self._recvmsg_called += 1
  258. if seq != self.FAKE_SEQUENCE:
  259. raise RuntimeError('unexpected CC sequence: ' + str(seq))
  260. if self._recvmsg_exception is not None:
  261. raise self._recvmsg_exception
  262. if self._answer_code is 0:
  263. return create_answer(0), None
  264. else:
  265. return create_answer(self._answer_code, "dummy error value"), None
  266. def clear_msg(self):
  267. '''Clear instrumental attributes related session messages.'''
  268. self._sent_msg = []
  269. self._recvmsg_called = 0
  270. self._answer_code = 0
  271. self._sendmsg_exception = None
  272. self._recvmsg_exception = None
  273. class MyDDNSServer():
  274. '''Fake DDNS server used to test the main() function'''
  275. def __init__(self):
  276. self.reset()
  277. def run(self):
  278. '''
  279. Fake the run() method of the DDNS server. This will set
  280. self._run_called to True.
  281. If self._exception is not None, this is raised as an exception
  282. '''
  283. self.run_called = True
  284. if self._exception is not None:
  285. self.exception_raised = True
  286. raise self._exception
  287. def set_exception(self, exception):
  288. '''Set an exception to be raised when run() is called'''
  289. self._exception = exception
  290. def reset(self):
  291. '''(Re)set to initial values'''
  292. self.run_called = False
  293. self.exception_raised = False
  294. self._exception = None
  295. class TestDDNSServer(unittest.TestCase):
  296. def setUp(self):
  297. cc_session = MyCCSession()
  298. self.assertFalse(cc_session._started)
  299. self.orig_tsig_keyring = isc.server_common.tsig_keyring
  300. isc.server_common.tsig_keyring = FakeKeyringModule()
  301. self.ddns_server = ddns.DDNSServer(cc_session)
  302. self.__cc_session = cc_session
  303. self.assertTrue(cc_session._started)
  304. self.__select_expected = None
  305. self.__select_answer = None
  306. self.__select_exception = None
  307. self.__hook_called = False
  308. self.ddns_server._listen_socket = FakeSocket(2)
  309. ddns.select.select = self.__select
  310. # common private attributes for TCP response tests
  311. self.__tcp_sock = FakeSocket(10, socket.IPPROTO_TCP)
  312. self.__tcp_ctx = DNSTCPContext(self.__tcp_sock)
  313. self.__tcp_data = b'A' * 12 # dummy, just the same size as DNS header
  314. # some tests will override this, which will be restored in tearDown:
  315. self.__orig_add_pause = ddns.add_pause
  316. def tearDown(self):
  317. ddns.select.select = select.select
  318. ddns.isc.util.cio.socketsession.SocketSessionReceiver = \
  319. isc.util.cio.socketsession.SocketSessionReceiver
  320. isc.server_common.tsig_keyring = self.orig_tsig_keyring
  321. ddns.add_pause = self.__orig_add_pause
  322. def test_listen(self):
  323. '''
  324. Test the old socket file is removed (if any) and a new socket
  325. is created when the ddns server is created.
  326. '''
  327. # Make sure the socket does not exist now
  328. ddns.clear_socket()
  329. # Hook the call for clearing the socket
  330. orig_clear = ddns.clear_socket
  331. ddns.clear_socket = self.__hook
  332. # Create the server
  333. ddnss = ddns.DDNSServer(MyCCSession())
  334. ddns.clear_socket = orig_clear
  335. # The socket is created
  336. self.assertTrue(os.path.exists(ddns.SOCKET_FILE))
  337. self.assertTrue(isinstance(ddnss._listen_socket, socket.socket))
  338. # And deletion of the socket was requested
  339. self.assertIsNone(self.__hook_called)
  340. # Now make sure the clear_socket really works
  341. ddns.clear_socket()
  342. self.assertFalse(os.path.exists(ddns.SOCKET_FILE))
  343. def test_initial_config(self):
  344. # right now, the only configuration is the zone configuration, whose
  345. # default should be an empty map.
  346. self.assertEqual({}, self.ddns_server._zone_config)
  347. def test_config_handler(self):
  348. # Update with a simple zone configuration: including an accept-all ACL
  349. new_config = { 'zones': [ { 'origin': TEST_ZONE_NAME_STR,
  350. 'class': TEST_RRCLASS_STR,
  351. 'update_acl': [{'action': 'ACCEPT'}] } ] }
  352. answer = self.ddns_server.config_handler(new_config)
  353. self.assertEqual((0, None), isc.config.parse_answer(answer))
  354. acl = self.ddns_server._zone_config[(TEST_ZONE_NAME, TEST_RRCLASS)]
  355. self.assertEqual(ACCEPT, acl.execute(TEST_ACL_CONTEXT))
  356. # Slightly more complicated one: containing multiple ACLs
  357. new_config = { 'zones': [ { 'origin': 'example.com',
  358. 'class': 'CH',
  359. 'update_acl': [{'action': 'REJECT',
  360. 'from': '2001:db8::1'}] },
  361. { 'origin': TEST_ZONE_NAME_STR,
  362. 'class': TEST_RRCLASS_STR,
  363. 'update_acl': [{'action': 'ACCEPT'}] },
  364. { 'origin': 'example.org',
  365. 'class': 'CH',
  366. 'update_acl': [{'action': 'DROP'}] } ] }
  367. answer = self.ddns_server.config_handler(new_config)
  368. self.assertEqual((0, None), isc.config.parse_answer(answer))
  369. self.assertEqual(3, len(self.ddns_server._zone_config))
  370. acl = self.ddns_server._zone_config[(TEST_ZONE_NAME, TEST_RRCLASS)]
  371. self.assertEqual(ACCEPT, acl.execute(TEST_ACL_CONTEXT))
  372. # empty zone config
  373. new_config = { 'zones': [] }
  374. answer = self.ddns_server.config_handler(new_config)
  375. self.assertEqual((0, None), isc.config.parse_answer(answer))
  376. self.assertEqual({}, self.ddns_server._zone_config)
  377. # bad zone config data: bad name. The previous config shouls be kept.
  378. bad_config = { 'zones': [ { 'origin': 'bad..example',
  379. 'class': TEST_RRCLASS_STR,
  380. 'update_acl': [{'action': 'ACCEPT'}] } ] }
  381. answer = self.ddns_server.config_handler(bad_config)
  382. self.assertEqual(1, isc.config.parse_answer(answer)[0])
  383. self.assertEqual({}, self.ddns_server._zone_config)
  384. # bad zone config data: bad class.
  385. bad_config = { 'zones': [ { 'origin': TEST_ZONE_NAME_STR,
  386. 'class': 'badclass',
  387. 'update_acl': [{'action': 'ACCEPT'}] } ] }
  388. answer = self.ddns_server.config_handler(bad_config)
  389. self.assertEqual(1, isc.config.parse_answer(answer)[0])
  390. self.assertEqual({}, self.ddns_server._zone_config)
  391. # bad zone config data: bad ACL.
  392. bad_config = { 'zones': [ { 'origin': TEST_ZONE_NAME_STR,
  393. 'class': TEST_RRCLASS_STR,
  394. 'update_acl': [{'action': 'badaction'}]}]}
  395. answer = self.ddns_server.config_handler(bad_config)
  396. self.assertEqual(1, isc.config.parse_answer(answer)[0])
  397. self.assertEqual({}, self.ddns_server._zone_config)
  398. # the first zone cofig is valid, but not the second. the first one
  399. # shouldn't be installed.
  400. bad_config = { 'zones': [ { 'origin': TEST_ZONE_NAME_STR,
  401. 'class': TEST_RRCLASS_STR,
  402. 'update_acl': [{'action': 'ACCEPT'}] },
  403. { 'origin': 'bad..example',
  404. 'class': TEST_RRCLASS_STR,
  405. 'update_acl': [{'action': 'ACCEPT'}] } ] }
  406. answer = self.ddns_server.config_handler(bad_config)
  407. self.assertEqual(1, isc.config.parse_answer(answer)[0])
  408. self.assertEqual({}, self.ddns_server._zone_config)
  409. # Half-broken case: 'origin, class' pair is duplicate. For now we
  410. # we accept it (the latter one will win)
  411. dup_config = { 'zones': [ { 'origin': TEST_ZONE_NAME_STR,
  412. 'class': TEST_RRCLASS_STR,
  413. 'update_acl': [{'action': 'REJECT'}] },
  414. { 'origin': TEST_ZONE_NAME_STR,
  415. 'class': TEST_RRCLASS_STR,
  416. 'update_acl': [{'action': 'ACCEPT'}] } ] }
  417. answer = self.ddns_server.config_handler(dup_config)
  418. self.assertEqual((0, None), isc.config.parse_answer(answer))
  419. acl = self.ddns_server._zone_config[(TEST_ZONE_NAME, TEST_RRCLASS)]
  420. self.assertEqual(ACCEPT, acl.execute(TEST_ACL_CONTEXT))
  421. def test_datasrc_config(self):
  422. # By default (in our faked config) it should be derived from the
  423. # test data source
  424. rrclass, datasrc_client = self.ddns_server._datasrc_info
  425. self.assertEqual(RRClass.IN(), rrclass)
  426. self.assertEqual(DataSourceClient.SUCCESS,
  427. datasrc_client.find_zone(Name('example.org'))[0])
  428. # emulating an update. calling add_remote_config_by_name is a
  429. # convenient faked way to invoke the callback. We set the db file
  430. # to a bogus one; the current implementation will create an unusable
  431. # data source client.
  432. self.__cc_session.auth_db_file = './notexistentdir/somedb.sqlite3'
  433. self.__cc_session._auth_config = \
  434. {'database_file': './notexistentdir/somedb.sqlite3'}
  435. self.__cc_session.add_remote_config_by_name('Auth')
  436. rrclass, datasrc_client = self.ddns_server._datasrc_info
  437. self.assertEqual(RRClass.IN(), rrclass)
  438. self.assertRaises(isc.datasrc.Error,
  439. datasrc_client.find_zone, Name('example.org'))
  440. # Check the current info isn't changed if the new config doesn't
  441. # update it.
  442. info_orig = self.ddns_server._datasrc_info
  443. self.ddns_server._datasrc_info = 42 # dummy value, should be kept.
  444. self.__cc_session._auth_config = {'other_config': 'value'}
  445. self.__cc_session.add_remote_config_by_name('Auth')
  446. self.assertEqual(42, self.ddns_server._datasrc_info)
  447. self.ddns_server._datasrc_info = info_orig
  448. def test_secondary_zones_config(self):
  449. # By default it should be an empty list
  450. self.assertEqual(set(), self.ddns_server._secondary_zones)
  451. # emulating an update.
  452. self.__cc_session._zonemgr_config = {'secondary_zones': [
  453. {'name': TEST_ZONE_NAME_STR, 'class': TEST_RRCLASS_STR}]}
  454. self.__cc_session.add_remote_config_by_name('Zonemgr')
  455. # The new set of secondary zones should be stored.
  456. self.assertEqual({(TEST_ZONE_NAME, TEST_RRCLASS)},
  457. self.ddns_server._secondary_zones)
  458. # Similar to the above, but 'class' is unspecified. The default value
  459. # should be used.
  460. self.__cc_session._zonemgr_config = {'secondary_zones': [
  461. {'name': TEST_ZONE_NAME_STR}]}
  462. self.__cc_session.add_remote_config_by_name('Zonemgr')
  463. self.assertEqual({(TEST_ZONE_NAME, TEST_RRCLASS)},
  464. self.ddns_server._secondary_zones)
  465. # The given list has a duplicate. The resulting set should unify them.
  466. self.__cc_session._zonemgr_config = {'secondary_zones': [
  467. {'name': TEST_ZONE_NAME_STR, 'class': TEST_RRCLASS_STR},
  468. {'name': TEST_ZONE_NAME_STR, 'class': TEST_RRCLASS_STR}]}
  469. self.__cc_session.add_remote_config_by_name('Zonemgr')
  470. self.assertEqual({(TEST_ZONE_NAME, TEST_RRCLASS)},
  471. self.ddns_server._secondary_zones)
  472. # Check the 2ndary zones aren't changed if the new config doesn't
  473. # update it.
  474. seczones_orig = self.ddns_server._secondary_zones
  475. self.ddns_server._secondary_zones = 42 # dummy value, should be kept.
  476. self.__cc_session._zonemgr_config = {}
  477. self.__cc_session.add_remote_config_by_name('Zonemgr')
  478. self.assertEqual(42, self.ddns_server._secondary_zones)
  479. self.ddns_server._secondary_zones = seczones_orig
  480. # If the update config is broken, the existing set should be intact.
  481. self.__cc_session._zonemgr_config = {'secondary_zones': [
  482. {'name': 'good.example', 'class': TEST_RRCLASS_STR},
  483. {'name': 'badd..example', 'class': TEST_RRCLASS_STR}]}
  484. self.__cc_session.add_remote_config_by_name('Zonemgr')
  485. self.assertEqual({(TEST_ZONE_NAME, TEST_RRCLASS)},
  486. self.ddns_server._secondary_zones)
  487. def __check_remote_config_fail(self, mod_name, num_ex, expected_ex):
  488. '''Subroutine for remote_config_fail test.'''
  489. # fake pause function for inspection and to avoid having timeouts
  490. added_pause = []
  491. ddns.add_pause = lambda sec: added_pause.append(sec)
  492. # In our current implementation, there will be up to 3 tries of
  493. # adding the module, each separated by a 1-sec pause. If all attempts
  494. # fail the exception will be propagated.
  495. exceptions = [expected_ex for i in range(0, num_ex)]
  496. self.__cc_session._raise_mods = {mod_name: exceptions}
  497. if num_ex >= 3:
  498. self.assertRaises(expected_ex, ddns.DDNSServer, self.__cc_session)
  499. else:
  500. ddns.DDNSServer(self.__cc_session)
  501. self.assertEqual([1 for i in range(0, num_ex)], added_pause)
  502. def test_remote_config_fail(self):
  503. # If getting config of Auth or Zonemgr fails on construction of
  504. # DDNServer, it should result in an exception and a few times
  505. # of retries. We test all possible cases, changing the number of
  506. # raised exceptions and the type of exceptions that can happen,
  507. # which should also cover the fatal error case.
  508. for i in range(0, 4):
  509. self.__check_remote_config_fail('Auth', i, ModuleCCSessionError)
  510. self.__check_remote_config_fail('Auth', i, ModuleSpecError)
  511. self.__check_remote_config_fail('Zonemgr', i, ModuleCCSessionError)
  512. self.__check_remote_config_fail('Zonemgr', i, ModuleSpecError)
  513. def test_shutdown_command(self):
  514. '''Test whether the shutdown command works'''
  515. self.assertFalse(self.ddns_server._shutdown)
  516. answer = self.ddns_server.command_handler('shutdown', None)
  517. self.assertEqual((0, None), isc.config.parse_answer(answer))
  518. self.assertTrue(self.ddns_server._shutdown)
  519. def test_command_handler(self):
  520. '''Test some commands.'''
  521. # this command should not exist
  522. answer = self.ddns_server.command_handler('bad_command', None)
  523. self.assertEqual((1, 'Unknown command: bad_command'),
  524. isc.config.parse_answer(answer))
  525. def test_signal_handler(self):
  526. '''Test whether signal_handler calls shutdown()'''
  527. signal_handler = ddns.create_signal_handler(self.ddns_server)
  528. self.assertFalse(self.ddns_server._shutdown)
  529. signal_handler(None, None)
  530. self.assertTrue(self.ddns_server._shutdown)
  531. def __select(self, reads, writes, exceptions, timeout=None):
  532. """
  533. A fake select. It checks it was called with the correct parameters and
  534. returns a preset answer.
  535. If there's an exception stored in __select_exception, it is raised
  536. instead and the exception is cleared.
  537. """
  538. self.assertEqual(self.__select_expected, (reads, writes, exceptions,
  539. timeout))
  540. if self.__select_exception is not None:
  541. (self.__select_exception, exception) = (None,
  542. self.__select_exception)
  543. raise exception
  544. answer = self.__select_answer
  545. self.__select_answer = None
  546. self.ddns_server._shutdown = True
  547. return answer
  548. def __hook(self, param=None):
  549. """
  550. A hook that can be installed to any nullary or unary function and see
  551. if it was really called.
  552. """
  553. self.__hook_called = param
  554. def test_accept_called(self):
  555. """
  556. Test we call the accept function when a new connection comes.
  557. """
  558. self.ddns_server.accept = self.__hook
  559. self.__select_expected = ([1, 2], [], [], None)
  560. self.__select_answer = ([2], [], [])
  561. self.__hook_called = "Not called"
  562. self.ddns_server.run()
  563. self.assertTrue(self.ddns_server._shutdown)
  564. # The answer got used
  565. self.assertIsNone(self.__select_answer)
  566. # Reset, when called without parameter
  567. self.assertIsNone(self.__hook_called)
  568. def test_check_command_called(self):
  569. """
  570. Test the check_command is called when there's something on the
  571. socket.
  572. """
  573. self.__cc_session.check_command = self.__hook
  574. self.__select_expected = ([1, 2], [], [], None)
  575. self.__select_answer = ([1], [], [])
  576. self.ddns_server.run()
  577. self.assertTrue(self.ddns_server._shutdown)
  578. # The answer got used
  579. self.assertIsNone(self.__select_answer)
  580. # And the check_command was called with true parameter (eg.
  581. # non-blocking)
  582. self.assertTrue(self.__hook_called)
  583. def test_accept(self):
  584. """
  585. Test that we can accept a new connection.
  586. """
  587. # There's nothing before the accept
  588. ddns.isc.util.cio.socketsession.SocketSessionReceiver = \
  589. FakeSessionReceiver
  590. self.assertEqual({}, self.ddns_server._socksession_receivers)
  591. self.ddns_server.accept()
  592. # Now the new socket session receiver is stored in the dict
  593. # The 3 comes from _listen_socket.accept() - _listen_socket has
  594. # fileno 2 and accept returns socket with fileno increased by one.
  595. self.assertEqual([3],
  596. list(self.ddns_server._socksession_receivers.keys()))
  597. (socket, receiver) = self.ddns_server._socksession_receivers[3]
  598. self.assertTrue(isinstance(socket, FakeSocket))
  599. self.assertEqual(3, socket.fileno())
  600. self.assertTrue(isinstance(receiver, FakeSessionReceiver))
  601. self.assertEqual(socket, receiver.socket())
  602. def test_accept_fail(self):
  603. """
  604. Test we don't crash if an accept fails and that we don't modify the
  605. internals.
  606. """
  607. # Make the accept fail
  608. def accept_failure():
  609. raise socket.error(errno.ECONNABORTED)
  610. orig = self.ddns_server._listen_socket.accept
  611. self.ddns_server._listen_socket.accept = accept_failure
  612. self.assertEqual({}, self.ddns_server._socksession_receivers)
  613. # Doesn't raise the exception
  614. self.ddns_server.accept()
  615. # And nothing is stored
  616. self.assertEqual({}, self.ddns_server._socksession_receivers)
  617. # Now make the socket receiver fail
  618. self.ddns_server._listen_socket.accept = orig
  619. def receiver_failure(sock):
  620. raise isc.util.cio.socketsession.SocketSessionError('Test error')
  621. ddns.isc.util.cio.socketsession.SocketSessionReceiver = \
  622. receiver_failure
  623. # Doesn't raise the exception
  624. self.ddns_server.accept()
  625. # And nothing is stored
  626. self.assertEqual({}, self.ddns_server._socksession_receivers)
  627. # Check we don't catch everything, so raise just an exception
  628. def unexpected_failure(sock):
  629. raise Exception('Test error')
  630. ddns.isc.util.cio.socketsession.SocketSessionReceiver = \
  631. unexpected_failure
  632. # This one gets through
  633. self.assertRaises(Exception, self.ddns_server.accept)
  634. # Nothing is stored as well
  635. self.assertEqual({}, self.ddns_server._socksession_receivers)
  636. def test_session_called(self):
  637. """
  638. Test the run calls handle_session when there's something on the
  639. socket.
  640. """
  641. socket = FakeSocket(3)
  642. self.ddns_server._socksession_receivers = \
  643. {3: (socket, FakeSessionReceiver(socket))}
  644. self.ddns_server.handle_session = self.__hook
  645. self.__select_expected = ([1, 2, 3], [], [], None)
  646. self.__select_answer = ([3], [], [])
  647. self.ddns_server.run()
  648. self.assertTrue(self.ddns_server._shutdown)
  649. self.assertTrue(self.__cc_session._stopped)
  650. self.assertIsNone(self.__select_answer)
  651. self.assertEqual(3, self.__hook_called)
  652. def test_handle_session_ok(self):
  653. """
  654. Test the handle_session pops the receiver and calls handle_request
  655. when everything is OK.
  656. """
  657. socket = FakeSocket(3)
  658. receiver = FakeSessionReceiver(socket)
  659. # It doesn't really matter what data we use here, it is only passed
  660. # through the code
  661. param = (FakeSocket(4), ('127.0.0.1', 1234), ('127.0.0.1', 1235),
  662. 'Some data')
  663. def pop():
  664. return param
  665. # Prepare data into the receiver
  666. receiver.pop = pop
  667. self.ddns_server._socksession_receivers = {3: (socket, receiver)}
  668. self.ddns_server.handle_request = self.__hook
  669. # Call it
  670. self.ddns_server.handle_session(3)
  671. # The popped data are passed into the handle_request
  672. self.assertEqual(param, self.__hook_called)
  673. # The receivers are kept the same
  674. self.assertEqual({3: (socket, receiver)},
  675. self.ddns_server._socksession_receivers)
  676. def test_handle_session_fail(self):
  677. """
  678. Test the handle_session removes (and closes) the socket and receiver
  679. when the receiver complains.
  680. """
  681. socket = FakeSocket(3)
  682. receiver = FakeSessionReceiver(socket)
  683. def pop():
  684. raise isc.util.cio.socketsession.SocketSessionError('Test error')
  685. receiver.pop = pop
  686. socket.close = self.__hook
  687. self.__hook_called = False
  688. self.ddns_server._socksession_receivers = {3: (socket, receiver)}
  689. self.ddns_server.handle_session(3)
  690. # The "dead" receiver is removed
  691. self.assertEqual({}, self.ddns_server._socksession_receivers)
  692. # Close is called with no parameter, so the default None
  693. self.assertIsNone(self.__hook_called)
  694. def test_select_exception_ignored(self):
  695. """
  696. Test that the EINTR is ignored in select.
  697. """
  698. # Prepare the EINTR exception
  699. self.__select_exception = select.error(errno.EINTR)
  700. # We reuse the test here, as it should act the same. The exception
  701. # should just get ignored.
  702. self.test_check_command_called()
  703. def test_select_exception_fatal(self):
  704. """
  705. Test that other exceptions are fatal to the run.
  706. """
  707. # Prepare a different exception
  708. self.__select_exception = select.error(errno.EBADF)
  709. self.__select_expected = ([1, 2], [], [], None)
  710. self.assertRaises(select.error, self.ddns_server.run)
  711. def __send_select_tcp(self, buflen, raise_after_select=False):
  712. '''Common subroutine for some TCP related tests below.'''
  713. fileno = self.__tcp_sock.fileno()
  714. self.ddns_server._tcp_ctxs = {fileno: (self.__tcp_ctx, TEST_CLIENT6)}
  715. # make an initial, incomplete send via the test context
  716. self.__tcp_sock._send_buflen = buflen
  717. self.assertEqual(DNSTCPContext.SENDING,
  718. self.__tcp_ctx.send(self.__tcp_data))
  719. self.assertEqual(buflen, len(self.__tcp_sock._sent_data))
  720. # clear the socket "send buffer"
  721. self.__tcp_sock.make_send_ready()
  722. # if requested, set up exception
  723. self.__tcp_sock._raise_on_send = raise_after_select
  724. # Run select
  725. self.__select_expected = ([1, 2], [fileno], [], None)
  726. self.__select_answer = ([], [fileno], [])
  727. self.ddns_server.run()
  728. def test_select_send_continued(self):
  729. '''Test continuation of sending a TCP response.'''
  730. # Common setup, with the bufsize that would make it complete after a
  731. # single select call.
  732. self.__send_select_tcp(7)
  733. # Now the send should be completed. socket should be closed,
  734. # and the context should be removed from the server.
  735. self.assertEqual(14, len(self.__tcp_sock._sent_data))
  736. self.assertEqual(1, self.__tcp_sock._close_called)
  737. self.assertEqual(0, len(self.ddns_server._tcp_ctxs))
  738. def test_select_send_continued_twice(self):
  739. '''Test continuation of sending a TCP response, still continuing.'''
  740. # This is similar to the send_continued test, but the continued
  741. # operation still won't complete the send.
  742. self.__send_select_tcp(5)
  743. # Only 10 bytes should have been transmitted, socket is still open,
  744. # and the context is still in the server (that would require select
  745. # watch it again).
  746. self.assertEqual(10, len(self.__tcp_sock._sent_data))
  747. self.assertEqual(0, self.__tcp_sock._close_called)
  748. fileno = self.__tcp_sock.fileno()
  749. self.assertEqual(self.__tcp_ctx,
  750. self.ddns_server._tcp_ctxs[fileno][0])
  751. def test_select_send_continued_failed(self):
  752. '''Test continuation of sending a TCP response, which fails.'''
  753. # Let the socket raise an exception in the second call to send().
  754. self.__send_select_tcp(5, raise_after_select=True)
  755. # Only the data before select() have been transmitted, socket is
  756. # closed due to the failure, and the context is removed from the
  757. # server.
  758. self.assertEqual(5, len(self.__tcp_sock._sent_data))
  759. self.assertEqual(1, self.__tcp_sock._close_called)
  760. self.assertEqual(0, len(self.ddns_server._tcp_ctxs))
  761. def test_select_multi_tcp(self):
  762. '''Test continuation of sending a TCP response, multiple sockets.'''
  763. # Check if the implementation still works with multiple outstanding
  764. # TCP contexts. We use three (arbitray choice), of which two will be
  765. # writable after select and complete the send.
  766. tcp_socks = []
  767. for i in range(0, 3):
  768. # Use faked FD of 100, 101, 102 (again, arbitrary choice)
  769. s = FakeSocket(100 + i, proto=socket.IPPROTO_TCP)
  770. ctx = DNSTCPContext(s)
  771. self.ddns_server._tcp_ctxs[s.fileno()] = (ctx, TEST_CLIENT6)
  772. s._send_buflen = 7 # make sure it requires two send's
  773. self.assertEqual(DNSTCPContext.SENDING, ctx.send(self.__tcp_data))
  774. s.make_send_ready()
  775. tcp_socks.append(s)
  776. self.__select_expected = ([1, 2], [100, 101, 102], [], None)
  777. self.__select_answer = ([], [100, 102], [])
  778. self.ddns_server.run()
  779. for i in [0, 2]:
  780. self.assertEqual(14, len(tcp_socks[i]._sent_data))
  781. self.assertEqual(1, tcp_socks[i]._close_called)
  782. self.assertEqual(1, len(self.ddns_server._tcp_ctxs))
  783. def test_select_bad_writefd(self):
  784. # There's no outstanding TCP context, but select somehow returns
  785. # writable FD. It should result in an uncaught exception, killing
  786. # the server. This is okay, because it shouldn't happen and should be
  787. # an internal bug.
  788. self.__select_expected = ([1, 2], [], [], None)
  789. self.__select_answer = ([], [10], [])
  790. self.assertRaises(KeyError, self.ddns_server.run)
  791. def create_msg(opcode=Opcode.UPDATE(), zones=[TEST_ZONE_RECORD], prereq=[],
  792. tsigctx=None):
  793. msg = Message(Message.RENDER)
  794. msg.set_qid(TEST_QID)
  795. msg.set_opcode(opcode)
  796. msg.set_rcode(Rcode.NOERROR())
  797. for z in zones:
  798. msg.add_question(z)
  799. for p in prereq:
  800. msg.add_rrset(SECTION_PREREQUISITE, p)
  801. renderer = MessageRenderer()
  802. if tsigctx is not None:
  803. msg.to_wire(renderer, tsigctx)
  804. else:
  805. msg.to_wire(renderer)
  806. # re-read the created data in the parse mode
  807. msg.clear(Message.PARSE)
  808. msg.from_wire(renderer.get_data())
  809. return renderer.get_data()
  810. class TestDDNSSession(unittest.TestCase):
  811. def setUp(self):
  812. self.__cc_session = MyCCSession()
  813. self.assertFalse(self.__cc_session._started)
  814. self.orig_tsig_keyring = isc.server_common.tsig_keyring
  815. isc.server_common.tsig_keyring = FakeKeyringModule()
  816. self.server = ddns.DDNSServer(self.__cc_session)
  817. self.server._UpdateSessionClass = self.__fake_session_creator
  818. self.__faked_result = UPDATE_SUCCESS # will be returned by fake session
  819. self.__sock = FakeSocket(-1)
  820. def tearDown(self):
  821. self.assertTrue(isc.server_common.tsig_keyring.initialized)
  822. isc.server_common.tsig_keyring = self.orig_tsig_keyring
  823. def __fake_session_creator(self, req_message, client_addr, zone_config):
  824. # remember the passed message for possible inspection later.
  825. self.__req_message = req_message
  826. return FakeUpdateSession(req_message, client_addr, zone_config,
  827. self.__faked_result)
  828. def check_update_response(self, resp_wire, expected_rcode=Rcode.NOERROR(),
  829. tsig_ctx=None, tcp=False):
  830. '''Check if given wire data are valid form of update response.
  831. In this implementation, zone/prerequisite/update sections should be
  832. empty in responses.
  833. If tsig_ctx (isc.dns.TSIGContext) is not None, the response should
  834. be TSIG signed and the signature should be verifiable with the context
  835. that has signed the corresponding request.
  836. if tcp is True, the wire data are expected to be prepended with
  837. a 2-byte length field.
  838. '''
  839. if tcp:
  840. data_len = resp_wire[0] * 256 + resp_wire[1]
  841. resp_wire = resp_wire[2:]
  842. self.assertEqual(len(resp_wire), data_len)
  843. msg = Message(Message.PARSE)
  844. msg.from_wire(resp_wire)
  845. if tsig_ctx is not None:
  846. tsig_record = msg.get_tsig_record()
  847. self.assertNotEqual(None, tsig_record)
  848. self.assertEqual(TSIGError.NOERROR,
  849. tsig_ctx.verify(tsig_record, resp_wire))
  850. self.assertEqual(Opcode.UPDATE(), msg.get_opcode())
  851. self.assertEqual(expected_rcode, msg.get_rcode())
  852. self.assertEqual(TEST_QID, msg.get_qid())
  853. for section in [SECTION_ZONE, SECTION_PREREQUISITE, SECTION_UPDATE]:
  854. self.assertEqual(0, msg.get_rr_count(section))
  855. def check_session(self, result=UPDATE_SUCCESS, ipv6=True, tsig_key=None):
  856. # reset test parameters
  857. self.__sock.clear()
  858. self.__faked_result = result
  859. server_addr = TEST_SERVER6 if ipv6 else TEST_SERVER4
  860. client_addr = TEST_CLIENT6 if ipv6 else TEST_CLIENT4
  861. tsig = TSIGContext(tsig_key) if tsig_key is not None else None
  862. rcode = Rcode.NOERROR() if result == UPDATE_SUCCESS else Rcode.REFUSED()
  863. has_response = (result != UPDATE_DROP)
  864. self.assertEqual(has_response,
  865. self.server.handle_request((self.__sock,
  866. server_addr, client_addr,
  867. create_msg(tsigctx=tsig))))
  868. if has_response:
  869. self.assertEqual(client_addr, self.__sock._sent_addr)
  870. self.check_update_response(self.__sock._sent_data, rcode)
  871. else:
  872. self.assertEqual((None, None), (self.__sock._sent_addr,
  873. self.__sock._sent_data))
  874. def test_handle_request(self):
  875. '''Basic request handling without any unexpected errors.'''
  876. # Success, without TSIG
  877. self.check_session()
  878. # Update will be refused with a response.
  879. self.check_session(UPDATE_ERROR, ipv6=False)
  880. # Update will be refused and dropped
  881. self.check_session(UPDATE_DROP)
  882. # Success, with TSIG
  883. self.check_session(ipv6=False, tsig_key=TEST_TSIG_KEY)
  884. # Update will be refused with a response, with TSIG.
  885. self.check_session(UPDATE_ERROR, tsig_key=TEST_TSIG_KEY)
  886. # Update will be refused and dropped, with TSIG (doesn't matter though)
  887. self.check_session(UPDATE_DROP, ipv6=False, tsig_key=TEST_TSIG_KEY)
  888. def test_broken_request(self):
  889. # Message data too short
  890. s = self.__sock
  891. self.assertFalse(self.server.handle_request((self.__sock, None,
  892. None, b'x' * 11)))
  893. self.assertEqual((None, None), (s._sent_data, s._sent_addr))
  894. # Opcode is not UPDATE
  895. self.assertFalse(self.server.handle_request(
  896. (self.__sock, None, None, create_msg(opcode=Opcode.QUERY()))))
  897. self.assertEqual((None, None), (s._sent_data, s._sent_addr))
  898. # TSIG verification error. We use UPDATE_DROP to signal check_session
  899. # that no response should be given.
  900. self.check_session(result=UPDATE_DROP, ipv6=False,
  901. tsig_key=BAD_TSIG_KEY)
  902. def test_socket_error(self):
  903. # Have the faked socket raise an exception on sendto()
  904. self.__sock._raise_on_send = True
  905. # handle_request indicates the failure
  906. self.assertFalse(self.server.handle_request((self.__sock, TEST_SERVER6,
  907. TEST_CLIENT6,
  908. create_msg())))
  909. # this check ensures sendto() was really attempted.
  910. self.check_update_response(self.__sock._sent_data, Rcode.NOERROR())
  911. def test_tcp_request(self):
  912. # A simple case using TCP: all resopnse data are sent out at once.
  913. s = self.__sock
  914. s.proto = socket.IPPROTO_TCP
  915. self.assertTrue(self.server.handle_request((s, TEST_SERVER6,
  916. TEST_CLIENT6,
  917. create_msg())))
  918. self.check_update_response(s._sent_data, Rcode.NOERROR(), tcp=True)
  919. # In the current implementation, the socket should be closed
  920. # immedidately after a successful send.
  921. self.assertEqual(1, s._close_called)
  922. # TCP context shouldn't be held in the server.
  923. self.assertEqual(0, len(self.server._tcp_ctxs))
  924. def test_tcp_request_incomplete(self):
  925. # set the size of the socket "send buffer" to a small value, which
  926. # should cause partial send.
  927. s = self.__sock
  928. s.proto = socket.IPPROTO_TCP
  929. s._send_buflen = 7
  930. # before this request there should be no outstanding TCP context.
  931. self.assertEqual(0, len(self.server._tcp_ctxs))
  932. self.assertTrue(self.server.handle_request((s, TEST_SERVER6,
  933. TEST_CLIENT6,
  934. create_msg())))
  935. # Only the part of data that fit the send buffer should be transmitted.
  936. self.assertEqual(s._send_buflen, len(s._sent_data))
  937. # the socket is not yet closed.
  938. self.assertEqual(0, s._close_called)
  939. # and a new context is stored in the server.
  940. self.assertEqual(1, len(self.server._tcp_ctxs))
  941. # clear the "send buffer" of the fake socket, and continue the send
  942. # by hand. The next attempt should complete the send, and the combined
  943. # data should be the expected response.
  944. s.make_send_ready()
  945. self.assertEqual(DNSTCPContext.SEND_DONE,
  946. self.server._tcp_ctxs[s.fileno()][0].send_ready())
  947. self.check_update_response(s._sent_data, Rcode.NOERROR(), tcp=True)
  948. def test_tcp_request_error(self):
  949. # initial send() on the TCP socket will fail. The request handling
  950. # will be considered failure.
  951. s = self.__sock
  952. s.proto = socket.IPPROTO_TCP
  953. s._raise_on_send = True
  954. self.assertFalse(self.server.handle_request((s, TEST_SERVER6,
  955. TEST_CLIENT6,
  956. create_msg())))
  957. # the socket should have been closed.
  958. self.assertEqual(1, s._close_called)
  959. def test_tcp_request_quota(self):
  960. '''Test'''
  961. # Originally the TCP context map should be empty.
  962. self.assertEqual(0, len(self.server._tcp_ctxs))
  963. class FakeReceiver:
  964. '''Faked SessionReceiver, just returning given param by pop()'''
  965. def __init__(self, param):
  966. self.__param = param
  967. def pop(self):
  968. return self.__param
  969. def check_tcp_ok(fd, expect_grant):
  970. '''Supplemental checker to see if TCP request is handled.'''
  971. s = FakeSocket(fd, proto=socket.IPPROTO_TCP)
  972. s._send_buflen = 7
  973. self.server._socksession_receivers[s.fileno()] = \
  974. (None, FakeReceiver((s, TEST_SERVER6, TEST_CLIENT6,
  975. create_msg())))
  976. self.assertEqual(expect_grant,
  977. self.server.handle_session(s.fileno()))
  978. self.assertEqual(0 if expect_grant else 1, s._close_called)
  979. # By default up to 10 TCP clients can coexist (use hardcode
  980. # intentionally so we can test the default value itself)
  981. for i in range(0, 10):
  982. check_tcp_ok(i, True)
  983. self.assertEqual(10, len(self.server._tcp_ctxs))
  984. # Beyond that, it should be rejected (by reset)
  985. check_tcp_ok(11, False)
  986. # If we remove one context from the server, new client can go in again.
  987. self.server._tcp_ctxs.pop(5)
  988. check_tcp_ok(12, True)
  989. def test_request_message(self):
  990. '''Test if the request message stores RRs separately.'''
  991. # Specify 'drop' so the passed message won't be modified.
  992. self.__faked_result = UPDATE_DROP
  993. # Put the same RR twice in the prerequisite section. We should see
  994. # them as separate RRs.
  995. dummy_record = RRset(TEST_ZONE_NAME, TEST_RRCLASS, RRType.NS(),
  996. RRTTL(0))
  997. dummy_record.add_rdata(Rdata(RRType.NS(), TEST_RRCLASS, "ns.example"))
  998. self.server.handle_request((self.__sock, TEST_SERVER6, TEST_CLIENT6,
  999. create_msg(prereq=[dummy_record,
  1000. dummy_record])))
  1001. num_rrsets = len(self.__req_message.get_section(SECTION_PREREQUISITE))
  1002. self.assertEqual(2, num_rrsets)
  1003. def check_session_msg(self, result, expect_recv=1, notify_auth=False):
  1004. '''Check post update communication with other modules.'''
  1005. # iff the update succeeds, b10-ddns should tell interested other
  1006. # modules the information about the update zone. Possible modules
  1007. # are xfrout and auth: for xfrout, the message format should be:
  1008. # {'command': ['notify', {'zone_name': <updated_zone_name>,
  1009. # 'zone_class', <updated_zone_class>}]}
  1010. # for auth, it should be:
  1011. # {'command': ['loadzone', {'origin': <updated_zone_name>,
  1012. # 'class', <updated_zone_class>,
  1013. # 'datasrc', <datasrc type, should be
  1014. # "memory" in practice>}]}
  1015. # and expect an answer by calling group_recvmsg().
  1016. #
  1017. # expect_recv indicates the expected number of calls to
  1018. # group_recvmsg(), which is normally 1, but can be 0 if send fails;
  1019. # if the message is to be sent
  1020. if result == UPDATE_SUCCESS:
  1021. expected_sentmsg = 2 if notify_auth else 1
  1022. self.assertEqual(expected_sentmsg,
  1023. len(self.__cc_session._sent_msg))
  1024. self.assertEqual(expect_recv, self.__cc_session._recvmsg_called)
  1025. msg_cnt = 0
  1026. if notify_auth:
  1027. sent_msg, sent_group = self.__cc_session._sent_msg[msg_cnt]
  1028. sent_cmd = sent_msg['command']
  1029. self.assertEqual('Auth', sent_group)
  1030. self.assertEqual('loadzone', sent_cmd[0])
  1031. self.assertEqual(3, len(sent_cmd[1]))
  1032. self.assertEqual(TEST_ZONE_NAME.to_text(),
  1033. sent_cmd[1]['origin'])
  1034. self.assertEqual(TEST_RRCLASS.to_text(),
  1035. sent_cmd[1]['class'])
  1036. self.assertEqual('memory', sent_cmd[1]['datasrc'])
  1037. msg_cnt += 1
  1038. sent_msg, sent_group = self.__cc_session._sent_msg[msg_cnt]
  1039. sent_cmd = sent_msg['command']
  1040. self.assertEqual('Xfrout', sent_group)
  1041. self.assertEqual('notify', sent_cmd[0])
  1042. self.assertEqual(2, len(sent_cmd[1]))
  1043. self.assertEqual(TEST_ZONE_NAME.to_text(), sent_cmd[1]['zone_name'])
  1044. self.assertEqual(TEST_RRCLASS.to_text(), sent_cmd[1]['zone_class'])
  1045. else:
  1046. # for other result cases neither send nor recvmsg should be called.
  1047. self.assertEqual([], self.__cc_session._sent_msg)
  1048. self.assertEqual(0, self.__cc_session._recvmsg_called)
  1049. def test_session_msg(self):
  1050. '''Test post update communication with other modules.'''
  1051. # Normal cases, confirming communication takes place iff update
  1052. # succeeds
  1053. for r in [UPDATE_SUCCESS, UPDATE_ERROR, UPDATE_DROP]:
  1054. self.__cc_session.clear_msg()
  1055. self.check_session(result=r)
  1056. self.check_session_msg(r)
  1057. # Return an error from the remote module, which should be just ignored.
  1058. self.__cc_session.clear_msg()
  1059. self.__cc_session._answer_code = 1
  1060. self.check_session()
  1061. self.check_session_msg(UPDATE_SUCCESS)
  1062. # raise some exceptions from the faked session. Expected ones are
  1063. # simply (logged and) ignored
  1064. self.__cc_session.clear_msg()
  1065. self.__cc_session._recvmsg_exception = SessionTimeout('dummy timeout')
  1066. self.check_session()
  1067. self.check_session_msg(UPDATE_SUCCESS)
  1068. self.__cc_session.clear_msg()
  1069. self.__cc_session._recvmsg_exception = SessionError('dummy error')
  1070. self.check_session()
  1071. self.check_session_msg(UPDATE_SUCCESS)
  1072. self.__cc_session.clear_msg()
  1073. self.__cc_session._recvmsg_exception = ProtocolError('dummy perror')
  1074. self.check_session()
  1075. self.check_session_msg(UPDATE_SUCCESS)
  1076. # Similar to the previous cases, but sendmsg() raises, so there should
  1077. # be no call to recvmsg().
  1078. self.__cc_session.clear_msg()
  1079. self.__cc_session._sendmsg_exception = SessionError('send error')
  1080. self.check_session()
  1081. self.check_session_msg(UPDATE_SUCCESS, expect_recv=0)
  1082. # Unexpected exception will be propagated (and will terminate the
  1083. # server)
  1084. self.__cc_session.clear_msg()
  1085. self.__cc_session._sendmsg_exception = RuntimeError('unexpected')
  1086. self.assertRaises(RuntimeError, self.check_session)
  1087. def test_session_msg_for_auth(self):
  1088. '''Test post update communication with other modules including Auth.'''
  1089. # Let the CC session return in-memory config with sqlite3 backend.
  1090. # (The default case was covered by other tests.)
  1091. self.__cc_session.auth_datasources = \
  1092. [{'type': 'memory', 'class': 'IN', 'zones': [
  1093. {'origin': TEST_ZONE_NAME_STR, 'filetype': 'sqlite3'}]}]
  1094. self.check_session()
  1095. self.check_session_msg(UPDATE_SUCCESS, expect_recv=2, notify_auth=True)
  1096. # Let sendmsg() raise an exception. The first exception shouldn't
  1097. # stop sending the second message. There's just no recv calls.
  1098. self.__cc_session.clear_msg()
  1099. self.__cc_session._sendmsg_exception = SessionError('send error')
  1100. self.check_session()
  1101. self.check_session_msg(UPDATE_SUCCESS, expect_recv=0, notify_auth=True)
  1102. # Likewise, in the case recvmsg() raises (and there should be recv
  1103. # calls in this case)
  1104. self.__cc_session.clear_msg()
  1105. self.__cc_session._recvmsg_exception = SessionError('recv error')
  1106. self.check_session()
  1107. self.check_session_msg(UPDATE_SUCCESS, expect_recv=2, notify_auth=True)
  1108. def test_session_with_config(self):
  1109. '''Check a session with more realistic config setups.
  1110. We don't have to explore various cases in detail in this test.
  1111. We're just checking if the expected configured objects are passed
  1112. to the session object.
  1113. '''
  1114. # reset the session class to the real one
  1115. self.server._UpdateSessionClass = isc.ddns.session.UpdateSession
  1116. # install all-drop ACL
  1117. new_config = { 'zones': [ { 'origin': TEST_ZONE_NAME_STR,
  1118. 'class': TEST_RRCLASS_STR,
  1119. 'update_acl': [{'action': 'DROP'}] } ] }
  1120. answer = self.server.config_handler(new_config)
  1121. self.assertEqual((0, None), isc.config.parse_answer(answer))
  1122. # check the result
  1123. self.check_session(UPDATE_DROP)
  1124. class TestMain(unittest.TestCase):
  1125. def setUp(self):
  1126. self._server = MyDDNSServer()
  1127. self.__orig_clear = ddns.clear_socket
  1128. ddns.clear_socket = self.__clear_socket
  1129. self.__clear_called = False
  1130. def tearDown(self):
  1131. ddns.clear_socket = self.__orig_clear
  1132. def test_main(self):
  1133. self.assertFalse(self._server.run_called)
  1134. ddns.main(self._server)
  1135. self.assertTrue(self._server.run_called)
  1136. self.assertTrue(self.__clear_called)
  1137. def __clear_socket(self):
  1138. self.__clear_called = True
  1139. # Get rid of the socket file too
  1140. self.__orig_clear()
  1141. def check_exception(self, ex):
  1142. '''Common test sequence to see if the given exception is caused.
  1143. '''
  1144. # Should technically not be necessary, but reset server to be sure
  1145. self._server.reset()
  1146. self.assertFalse(self._server.exception_raised)
  1147. self._server.set_exception(ex)
  1148. ddns.main(self._server)
  1149. self.assertTrue(self._server.exception_raised)
  1150. def test_exceptions(self):
  1151. '''
  1152. Test whether exceptions are caught in main()
  1153. These exceptions should not bubble up.
  1154. '''
  1155. self._server.set_exception(KeyboardInterrupt())
  1156. self.assertFalse(self._server.exception_raised)
  1157. ddns.main(self._server)
  1158. self.assertTrue(self._server.exception_raised)
  1159. self.check_exception(isc.cc.SessionError("error"))
  1160. self.check_exception(isc.config.ModuleCCSessionError("error"))
  1161. self.check_exception(ddns.DDNSConfigError("error"))
  1162. self.check_exception(isc.cc.SessionTimeout("error"))
  1163. self.check_exception(Exception("error"))
  1164. # Add one that is not a subclass of Exception, and hence not
  1165. # caught. Misuse BaseException for that.
  1166. self._server.reset()
  1167. self.assertFalse(self._server.exception_raised)
  1168. self._server.set_exception(BaseException("error"))
  1169. self.assertRaises(BaseException, ddns.main, self._server)
  1170. self.assertTrue(self._server.exception_raised)
  1171. class TestConfig(unittest.TestCase):
  1172. '''Test some simple config related things that don't need server. '''
  1173. def setUp(self):
  1174. self.__ccsession = MyCCSession()
  1175. def test_file_path(self):
  1176. # Check some common paths
  1177. self.assertEqual(os.environ["B10_FROM_BUILD"] + "/ddns_socket",
  1178. ddns.SOCKET_FILE)
  1179. self.assertEqual(os.environ["B10_FROM_SOURCE"] +
  1180. "/src/bin/ddns/ddns.spec", ddns.SPECFILE_LOCATION)
  1181. if __name__== "__main__":
  1182. isc.log.resetUnitTestRootLogger()
  1183. unittest.main()