|
@@ -19,7 +19,10 @@ from isc.ddns.session import *
|
|
from isc.dns import *
|
|
from isc.dns import *
|
|
from isc.acl.acl import ACCEPT
|
|
from isc.acl.acl import ACCEPT
|
|
import isc.util.cio.socketsession
|
|
import isc.util.cio.socketsession
|
|
|
|
+from isc.cc.session import SessionTimeout, SessionError, ProtocolError
|
|
from isc.datasrc import DataSourceClient
|
|
from isc.datasrc import DataSourceClient
|
|
|
|
+from isc.config.ccsession import create_answer
|
|
|
|
+from isc.server_common.dns_tcp import DNSTCPContext
|
|
import ddns
|
|
import ddns
|
|
import errno
|
|
import errno
|
|
import os
|
|
import os
|
|
@@ -57,17 +60,23 @@ class FakeSocket:
|
|
"""
|
|
"""
|
|
A fake socket. It only provides a file number, peer name and accept method.
|
|
A fake socket. It only provides a file number, peer name and accept method.
|
|
"""
|
|
"""
|
|
- def __init__(self, fileno):
|
|
|
|
- self.proto = socket.IPPROTO_UDP
|
|
|
|
|
|
+ def __init__(self, fileno, proto=socket.IPPROTO_UDP):
|
|
|
|
+ self.proto = proto
|
|
self.__fileno = fileno
|
|
self.__fileno = fileno
|
|
self._sent_data = None
|
|
self._sent_data = None
|
|
self._sent_addr = None
|
|
self._sent_addr = None
|
|
|
|
+ self._close_called = 0 # number of calls to close()
|
|
|
|
+ self.__send_cc = 0 # waterline of the send buffer (can be reset)
|
|
# customizable by tests; if set to True, sendto() will throw after
|
|
# customizable by tests; if set to True, sendto() will throw after
|
|
# recording the parameters.
|
|
# recording the parameters.
|
|
self._raise_on_send = False
|
|
self._raise_on_send = False
|
|
|
|
+ self._send_buflen = None # imaginary send buffer for partial send
|
|
def fileno(self):
|
|
def fileno(self):
|
|
return self.__fileno
|
|
return self.__fileno
|
|
def getpeername(self):
|
|
def getpeername(self):
|
|
|
|
+ if self.proto == socket.IPPROTO_UDP or \
|
|
|
|
+ self.proto == socket.IPPROTO_TCP:
|
|
|
|
+ return TEST_CLIENT4
|
|
return "fake_unix_socket"
|
|
return "fake_unix_socket"
|
|
def accept(self):
|
|
def accept(self):
|
|
return FakeSocket(self.__fileno + 1), '/dummy/path'
|
|
return FakeSocket(self.__fileno + 1), '/dummy/path'
|
|
@@ -76,10 +85,39 @@ class FakeSocket:
|
|
self._sent_addr = addr
|
|
self._sent_addr = addr
|
|
if self._raise_on_send:
|
|
if self._raise_on_send:
|
|
raise socket.error('test socket failure')
|
|
raise socket.error('test socket failure')
|
|
|
|
+ def send(self, data):
|
|
|
|
+ if self._raise_on_send:
|
|
|
|
+ raise socket.error(errno.EPIPE, 'faked connection disruption')
|
|
|
|
+ elif self._send_buflen is None:
|
|
|
|
+ available_space = len(data)
|
|
|
|
+ else:
|
|
|
|
+ available_space = self._send_buflen - self.__send_cc
|
|
|
|
+ if available_space == 0:
|
|
|
|
+ # if there's no space, (assuming it's nonblocking mode) raise
|
|
|
|
+ # EAGAIN.
|
|
|
|
+ raise socket.error(errno.EAGAIN,
|
|
|
|
+ "Resource temporarily unavailable")
|
|
|
|
+ # determine the sendable part of the data, record it, update "buffer".
|
|
|
|
+ cc = min(available_space, len(data))
|
|
|
|
+ if self._sent_data is None:
|
|
|
|
+ self._sent_data = data[:cc]
|
|
|
|
+ else:
|
|
|
|
+ self._sent_data += data[:cc]
|
|
|
|
+ self.__send_cc += cc
|
|
|
|
+ return cc
|
|
|
|
+ def setblocking(self, on):
|
|
|
|
+ # We only need a faked NO-OP implementation.
|
|
|
|
+ pass
|
|
|
|
+ def close(self):
|
|
|
|
+ self._close_called += 1
|
|
def clear(self):
|
|
def clear(self):
|
|
'''Clear internal instrumental data.'''
|
|
'''Clear internal instrumental data.'''
|
|
self._sent_data = None
|
|
self._sent_data = None
|
|
self._sent_addr = None
|
|
self._sent_addr = None
|
|
|
|
+ def make_send_ready(self):
|
|
|
|
+ # pretend that the accrued data has been cleared, making room in
|
|
|
|
+ # the send buffer.
|
|
|
|
+ self.__send_cc = 0
|
|
|
|
|
|
class FakeSessionReceiver:
|
|
class FakeSessionReceiver:
|
|
"""
|
|
"""
|
|
@@ -147,6 +185,10 @@ class FakeKeyringModule:
|
|
|
|
|
|
class MyCCSession(isc.config.ConfigData):
|
|
class MyCCSession(isc.config.ConfigData):
|
|
'''Fake session with minimal interface compliance.'''
|
|
'''Fake session with minimal interface compliance.'''
|
|
|
|
+
|
|
|
|
+ # faked CC sequence used in group_send/recvmsg
|
|
|
|
+ FAKE_SEQUENCE = 53
|
|
|
|
+
|
|
def __init__(self):
|
|
def __init__(self):
|
|
module_spec = isc.config.module_spec_from_file(
|
|
module_spec = isc.config.module_spec_from_file(
|
|
ddns.SPECFILE_LOCATION)
|
|
ddns.SPECFILE_LOCATION)
|
|
@@ -155,6 +197,16 @@ class MyCCSession(isc.config.ConfigData):
|
|
self._stopped = False
|
|
self._stopped = False
|
|
# Used as the return value of get_remote_config_value. Customizable.
|
|
# Used as the return value of get_remote_config_value. Customizable.
|
|
self.auth_db_file = READ_ZONE_DB_FILE
|
|
self.auth_db_file = READ_ZONE_DB_FILE
|
|
|
|
+ # Used as the return value of get_remote_config_value. Customizable.
|
|
|
|
+ self.auth_datasources = None
|
|
|
|
+ # faked cc channel, providing group_send/recvmsg itself. The following
|
|
|
|
+ # attributes are for inspection/customization in tests.
|
|
|
|
+ self._session = self
|
|
|
|
+ self._sent_msg = []
|
|
|
|
+ self._recvmsg_called = 0
|
|
|
|
+ self._answer_code = 0 # code used in answer returned via recvmsg
|
|
|
|
+ self._sendmsg_exception = None # will be raised from sendmsg if !None
|
|
|
|
+ self._recvmsg_exception = None # will be raised from recvmsg if !None
|
|
|
|
|
|
def start(self):
|
|
def start(self):
|
|
'''Called by DDNSServer initialization, but not used in tests'''
|
|
'''Called by DDNSServer initialization, but not used in tests'''
|
|
@@ -176,6 +228,37 @@ class MyCCSession(isc.config.ConfigData):
|
|
def get_remote_config_value(self, module_name, item):
|
|
def get_remote_config_value(self, module_name, item):
|
|
if module_name == "Auth" and item == "database_file":
|
|
if module_name == "Auth" and item == "database_file":
|
|
return self.auth_db_file, False
|
|
return self.auth_db_file, False
|
|
|
|
+ if module_name == "Auth" and item == "datasources":
|
|
|
|
+ if self.auth_datasources is None:
|
|
|
|
+ return [], True # default
|
|
|
|
+ else:
|
|
|
|
+ return self.auth_datasources, False
|
|
|
|
+
|
|
|
|
+ def group_sendmsg(self, msg, group):
|
|
|
|
+ # remember the passed parameter, and return dummy sequence
|
|
|
|
+ self._sent_msg.append((msg, group))
|
|
|
|
+ if self._sendmsg_exception is not None:
|
|
|
|
+ raise self._sendmsg_exception
|
|
|
|
+ return self.FAKE_SEQUENCE
|
|
|
|
+
|
|
|
|
+ def group_recvmsg(self, nonblock, seq):
|
|
|
|
+ self._recvmsg_called += 1
|
|
|
|
+ if seq != self.FAKE_SEQUENCE:
|
|
|
|
+ raise RuntimeError('unexpected CC sequence: ' + str(seq))
|
|
|
|
+ if self._recvmsg_exception is not None:
|
|
|
|
+ raise self._recvmsg_exception
|
|
|
|
+ if self._answer_code is 0:
|
|
|
|
+ return create_answer(0), None
|
|
|
|
+ else:
|
|
|
|
+ return create_answer(self._answer_code, "dummy error value"), None
|
|
|
|
+
|
|
|
|
+ def clear_msg(self):
|
|
|
|
+ '''Clear instrumental attributes related session messages.'''
|
|
|
|
+ self._sent_msg = []
|
|
|
|
+ self._recvmsg_called = 0
|
|
|
|
+ self._answer_code = 0
|
|
|
|
+ self._sendmsg_exception = None
|
|
|
|
+ self._recvmsg_exception = None
|
|
|
|
|
|
class MyDDNSServer():
|
|
class MyDDNSServer():
|
|
'''Fake DDNS server used to test the main() function'''
|
|
'''Fake DDNS server used to test the main() function'''
|
|
@@ -219,6 +302,11 @@ class TestDDNSServer(unittest.TestCase):
|
|
self.ddns_server._listen_socket = FakeSocket(2)
|
|
self.ddns_server._listen_socket = FakeSocket(2)
|
|
ddns.select.select = self.__select
|
|
ddns.select.select = self.__select
|
|
|
|
|
|
|
|
+ # common private attributes for TCP response tests
|
|
|
|
+ self.__tcp_sock = FakeSocket(10, socket.IPPROTO_TCP)
|
|
|
|
+ self.__tcp_ctx = DNSTCPContext(self.__tcp_sock)
|
|
|
|
+ self.__tcp_data = b'A' * 12 # dummy, just the same size as DNS header
|
|
|
|
+
|
|
def tearDown(self):
|
|
def tearDown(self):
|
|
ddns.select.select = select.select
|
|
ddns.select.select = select.select
|
|
ddns.isc.util.cio.socketsession.SocketSessionReceiver = \
|
|
ddns.isc.util.cio.socketsession.SocketSessionReceiver = \
|
|
@@ -547,6 +635,100 @@ class TestDDNSServer(unittest.TestCase):
|
|
self.__select_expected = ([1, 2], [], [], None)
|
|
self.__select_expected = ([1, 2], [], [], None)
|
|
self.assertRaises(select.error, self.ddns_server.run)
|
|
self.assertRaises(select.error, self.ddns_server.run)
|
|
|
|
|
|
|
|
+ def __send_select_tcp(self, buflen, raise_after_select=False):
|
|
|
|
+ '''Common subroutine for some TCP related tests below.'''
|
|
|
|
+ fileno = self.__tcp_sock.fileno()
|
|
|
|
+ self.ddns_server._tcp_ctxs = {fileno: (self.__tcp_ctx, TEST_CLIENT6)}
|
|
|
|
+
|
|
|
|
+ # make an initial, incomplete send via the test context
|
|
|
|
+ self.__tcp_sock._send_buflen = buflen
|
|
|
|
+ self.assertEqual(DNSTCPContext.SENDING,
|
|
|
|
+ self.__tcp_ctx.send(self.__tcp_data))
|
|
|
|
+ self.assertEqual(buflen, len(self.__tcp_sock._sent_data))
|
|
|
|
+ # clear the socket "send buffer"
|
|
|
|
+ self.__tcp_sock.make_send_ready()
|
|
|
|
+ # if requested, set up exception
|
|
|
|
+ self.__tcp_sock._raise_on_send = raise_after_select
|
|
|
|
+
|
|
|
|
+ # Run select
|
|
|
|
+ self.__select_expected = ([1, 2], [fileno], [], None)
|
|
|
|
+ self.__select_answer = ([], [fileno], [])
|
|
|
|
+ self.ddns_server.run()
|
|
|
|
+
|
|
|
|
+ def test_select_send_continued(self):
|
|
|
|
+ '''Test continuation of sending a TCP response.'''
|
|
|
|
+ # Common setup, with the bufsize that would make it complete after a
|
|
|
|
+ # single select call.
|
|
|
|
+ self.__send_select_tcp(7)
|
|
|
|
+
|
|
|
|
+ # Now the send should be completed. socket should be closed,
|
|
|
|
+ # and the context should be removed from the server.
|
|
|
|
+ self.assertEqual(14, len(self.__tcp_sock._sent_data))
|
|
|
|
+ self.assertEqual(1, self.__tcp_sock._close_called)
|
|
|
|
+ self.assertEqual(0, len(self.ddns_server._tcp_ctxs))
|
|
|
|
+
|
|
|
|
+ def test_select_send_continued_twice(self):
|
|
|
|
+ '''Test continuation of sending a TCP response, still continuing.'''
|
|
|
|
+ # This is similar to the send_continued test, but the continued
|
|
|
|
+ # operation still won't complete the send.
|
|
|
|
+ self.__send_select_tcp(5)
|
|
|
|
+
|
|
|
|
+ # Only 10 bytes should have been transmitted, socket is still open,
|
|
|
|
+ # and the context is still in the server (that would require select
|
|
|
|
+ # watch it again).
|
|
|
|
+ self.assertEqual(10, len(self.__tcp_sock._sent_data))
|
|
|
|
+ self.assertEqual(0, self.__tcp_sock._close_called)
|
|
|
|
+ fileno = self.__tcp_sock.fileno()
|
|
|
|
+ self.assertEqual(self.__tcp_ctx,
|
|
|
|
+ self.ddns_server._tcp_ctxs[fileno][0])
|
|
|
|
+
|
|
|
|
+ def test_select_send_continued_failed(self):
|
|
|
|
+ '''Test continuation of sending a TCP response, which fails.'''
|
|
|
|
+ # Let the socket raise an exception in the second call to send().
|
|
|
|
+ self.__send_select_tcp(5, raise_after_select=True)
|
|
|
|
+
|
|
|
|
+ # Only the data before select() have been transmitted, socket is
|
|
|
|
+ # closed due to the failure, and the context is removed from the
|
|
|
|
+ # server.
|
|
|
|
+ self.assertEqual(5, len(self.__tcp_sock._sent_data))
|
|
|
|
+ self.assertEqual(1, self.__tcp_sock._close_called)
|
|
|
|
+ self.assertEqual(0, len(self.ddns_server._tcp_ctxs))
|
|
|
|
+
|
|
|
|
+ def test_select_multi_tcp(self):
|
|
|
|
+ '''Test continuation of sending a TCP response, multiple sockets.'''
|
|
|
|
+ # Check if the implementation still works with multiple outstanding
|
|
|
|
+ # TCP contexts. We use three (arbitray choice), of which two will be
|
|
|
|
+ # writable after select and complete the send.
|
|
|
|
+ tcp_socks = []
|
|
|
|
+ for i in range(0, 3):
|
|
|
|
+ # Use faked FD of 100, 101, 102 (again, arbitrary choice)
|
|
|
|
+ s = FakeSocket(100 + i, proto=socket.IPPROTO_TCP)
|
|
|
|
+ ctx = DNSTCPContext(s)
|
|
|
|
+ self.ddns_server._tcp_ctxs[s.fileno()] = (ctx, TEST_CLIENT6)
|
|
|
|
+ s._send_buflen = 7 # make sure it requires two send's
|
|
|
|
+ self.assertEqual(DNSTCPContext.SENDING, ctx.send(self.__tcp_data))
|
|
|
|
+ s.make_send_ready()
|
|
|
|
+
|
|
|
|
+ tcp_socks.append(s)
|
|
|
|
+
|
|
|
|
+ self.__select_expected = ([1, 2], [100, 101, 102], [], None)
|
|
|
|
+ self.__select_answer = ([], [100, 102], [])
|
|
|
|
+ self.ddns_server.run()
|
|
|
|
+
|
|
|
|
+ for i in [0, 2]:
|
|
|
|
+ self.assertEqual(14, len(tcp_socks[i]._sent_data))
|
|
|
|
+ self.assertEqual(1, tcp_socks[i]._close_called)
|
|
|
|
+ self.assertEqual(1, len(self.ddns_server._tcp_ctxs))
|
|
|
|
+
|
|
|
|
+ def test_select_bad_writefd(self):
|
|
|
|
+ # There's no outstanding TCP context, but select somehow returns
|
|
|
|
+ # writable FD. It should result in an uncaught exception, killing
|
|
|
|
+ # the server. This is okay, because it shouldn't happen and should be
|
|
|
|
+ # an internal bug.
|
|
|
|
+ self.__select_expected = ([1, 2], [], [], None)
|
|
|
|
+ self.__select_answer = ([], [10], [])
|
|
|
|
+ self.assertRaises(KeyError, self.ddns_server.run)
|
|
|
|
+
|
|
def create_msg(opcode=Opcode.UPDATE(), zones=[TEST_ZONE_RECORD], prereq=[],
|
|
def create_msg(opcode=Opcode.UPDATE(), zones=[TEST_ZONE_RECORD], prereq=[],
|
|
tsigctx=None):
|
|
tsigctx=None):
|
|
msg = Message(Message.RENDER)
|
|
msg = Message(Message.RENDER)
|
|
@@ -571,13 +753,13 @@ def create_msg(opcode=Opcode.UPDATE(), zones=[TEST_ZONE_RECORD], prereq=[],
|
|
return renderer.get_data()
|
|
return renderer.get_data()
|
|
|
|
|
|
|
|
|
|
-class TestDDNSession(unittest.TestCase):
|
|
|
|
|
|
+class TestDDNSSession(unittest.TestCase):
|
|
def setUp(self):
|
|
def setUp(self):
|
|
- cc_session = MyCCSession()
|
|
|
|
- self.assertFalse(cc_session._started)
|
|
|
|
|
|
+ self.__cc_session = MyCCSession()
|
|
|
|
+ self.assertFalse(self.__cc_session._started)
|
|
self.orig_tsig_keyring = isc.server_common.tsig_keyring
|
|
self.orig_tsig_keyring = isc.server_common.tsig_keyring
|
|
isc.server_common.tsig_keyring = FakeKeyringModule()
|
|
isc.server_common.tsig_keyring = FakeKeyringModule()
|
|
- self.server = ddns.DDNSServer(cc_session)
|
|
|
|
|
|
+ self.server = ddns.DDNSServer(self.__cc_session)
|
|
self.server._UpdateSessionClass = self.__fake_session_creator
|
|
self.server._UpdateSessionClass = self.__fake_session_creator
|
|
self.__faked_result = UPDATE_SUCCESS # will be returned by fake session
|
|
self.__faked_result = UPDATE_SUCCESS # will be returned by fake session
|
|
self.__sock = FakeSocket(-1)
|
|
self.__sock = FakeSocket(-1)
|
|
@@ -593,7 +775,7 @@ class TestDDNSession(unittest.TestCase):
|
|
self.__faked_result)
|
|
self.__faked_result)
|
|
|
|
|
|
def check_update_response(self, resp_wire, expected_rcode=Rcode.NOERROR(),
|
|
def check_update_response(self, resp_wire, expected_rcode=Rcode.NOERROR(),
|
|
- tsig_ctx=None):
|
|
|
|
|
|
+ tsig_ctx=None, tcp=False):
|
|
'''Check if given wire data are valid form of update response.
|
|
'''Check if given wire data are valid form of update response.
|
|
|
|
|
|
In this implementation, zone/prerequisite/update sections should be
|
|
In this implementation, zone/prerequisite/update sections should be
|
|
@@ -603,7 +785,15 @@ class TestDDNSession(unittest.TestCase):
|
|
be TSIG signed and the signature should be verifiable with the context
|
|
be TSIG signed and the signature should be verifiable with the context
|
|
that has signed the corresponding request.
|
|
that has signed the corresponding request.
|
|
|
|
|
|
|
|
+ if tcp is True, the wire data are expected to be prepended with
|
|
|
|
+ a 2-byte length field.
|
|
|
|
+
|
|
'''
|
|
'''
|
|
|
|
+ if tcp:
|
|
|
|
+ data_len = resp_wire[0] * 256 + resp_wire[1]
|
|
|
|
+ resp_wire = resp_wire[2:]
|
|
|
|
+ self.assertEqual(len(resp_wire), data_len)
|
|
|
|
+
|
|
msg = Message(Message.PARSE)
|
|
msg = Message(Message.PARSE)
|
|
msg.from_wire(resp_wire)
|
|
msg.from_wire(resp_wire)
|
|
if tsig_ctx is not None:
|
|
if tsig_ctx is not None:
|
|
@@ -676,19 +866,98 @@ class TestDDNSession(unittest.TestCase):
|
|
self.__sock._raise_on_send = True
|
|
self.__sock._raise_on_send = True
|
|
# handle_request indicates the failure
|
|
# handle_request indicates the failure
|
|
self.assertFalse(self.server.handle_request((self.__sock, TEST_SERVER6,
|
|
self.assertFalse(self.server.handle_request((self.__sock, TEST_SERVER6,
|
|
- TEST_SERVER4,
|
|
|
|
|
|
+ TEST_CLIENT6,
|
|
create_msg())))
|
|
create_msg())))
|
|
# this check ensures sendto() was really attempted.
|
|
# this check ensures sendto() was really attempted.
|
|
self.check_update_response(self.__sock._sent_data, Rcode.NOERROR())
|
|
self.check_update_response(self.__sock._sent_data, Rcode.NOERROR())
|
|
|
|
|
|
def test_tcp_request(self):
|
|
def test_tcp_request(self):
|
|
- # Right now TCP request is not supported.
|
|
|
|
|
|
+ # A simple case using TCP: all resopnse data are sent out at once.
|
|
s = self.__sock
|
|
s = self.__sock
|
|
s.proto = socket.IPPROTO_TCP
|
|
s.proto = socket.IPPROTO_TCP
|
|
|
|
+ self.assertTrue(self.server.handle_request((s, TEST_SERVER6,
|
|
|
|
+ TEST_CLIENT6,
|
|
|
|
+ create_msg())))
|
|
|
|
+ self.check_update_response(s._sent_data, Rcode.NOERROR(), tcp=True)
|
|
|
|
+ # In the current implementation, the socket should be closed
|
|
|
|
+ # immedidately after a successful send.
|
|
|
|
+ self.assertEqual(1, s._close_called)
|
|
|
|
+ # TCP context shouldn't be held in the server.
|
|
|
|
+ self.assertEqual(0, len(self.server._tcp_ctxs))
|
|
|
|
+
|
|
|
|
+ def test_tcp_request_incomplete(self):
|
|
|
|
+ # set the size of the socket "send buffer" to a small value, which
|
|
|
|
+ # should cause partial send.
|
|
|
|
+ s = self.__sock
|
|
|
|
+ s.proto = socket.IPPROTO_TCP
|
|
|
|
+ s._send_buflen = 7
|
|
|
|
+ # before this request there should be no outstanding TCP context.
|
|
|
|
+ self.assertEqual(0, len(self.server._tcp_ctxs))
|
|
|
|
+ self.assertTrue(self.server.handle_request((s, TEST_SERVER6,
|
|
|
|
+ TEST_CLIENT6,
|
|
|
|
+ create_msg())))
|
|
|
|
+ # Only the part of data that fit the send buffer should be transmitted.
|
|
|
|
+ self.assertEqual(s._send_buflen, len(s._sent_data))
|
|
|
|
+ # the socket is not yet closed.
|
|
|
|
+ self.assertEqual(0, s._close_called)
|
|
|
|
+ # and a new context is stored in the server.
|
|
|
|
+ self.assertEqual(1, len(self.server._tcp_ctxs))
|
|
|
|
+
|
|
|
|
+ # clear the "send buffer" of the fake socket, and continue the send
|
|
|
|
+ # by hand. The next attempt should complete the send, and the combined
|
|
|
|
+ # data should be the expected response.
|
|
|
|
+ s.make_send_ready()
|
|
|
|
+ self.assertEqual(DNSTCPContext.SEND_DONE,
|
|
|
|
+ self.server._tcp_ctxs[s.fileno()][0].send_ready())
|
|
|
|
+ self.check_update_response(s._sent_data, Rcode.NOERROR(), tcp=True)
|
|
|
|
+
|
|
|
|
+ def test_tcp_request_error(self):
|
|
|
|
+ # initial send() on the TCP socket will fail. The request handling
|
|
|
|
+ # will be considered failure.
|
|
|
|
+ s = self.__sock
|
|
|
|
+ s.proto = socket.IPPROTO_TCP
|
|
|
|
+ s._raise_on_send = True
|
|
self.assertFalse(self.server.handle_request((s, TEST_SERVER6,
|
|
self.assertFalse(self.server.handle_request((s, TEST_SERVER6,
|
|
- TEST_SERVER4,
|
|
|
|
|
|
+ TEST_CLIENT6,
|
|
create_msg())))
|
|
create_msg())))
|
|
- self.assertEqual((None, None), (s._sent_data, s._sent_addr))
|
|
|
|
|
|
+ # the socket should have been closed.
|
|
|
|
+ self.assertEqual(1, s._close_called)
|
|
|
|
+
|
|
|
|
+ def test_tcp_request_quota(self):
|
|
|
|
+ '''Test'''
|
|
|
|
+ # Originally the TCP context map should be empty.
|
|
|
|
+ self.assertEqual(0, len(self.server._tcp_ctxs))
|
|
|
|
+
|
|
|
|
+ class FakeReceiver:
|
|
|
|
+ '''Faked SessionReceiver, just returning given param by pop()'''
|
|
|
|
+ def __init__(self, param):
|
|
|
|
+ self.__param = param
|
|
|
|
+ def pop(self):
|
|
|
|
+ return self.__param
|
|
|
|
+
|
|
|
|
+ def check_tcp_ok(fd, expect_grant):
|
|
|
|
+ '''Supplemental checker to see if TCP request is handled.'''
|
|
|
|
+ s = FakeSocket(fd, proto=socket.IPPROTO_TCP)
|
|
|
|
+ s._send_buflen = 7
|
|
|
|
+ self.server._socksession_receivers[s.fileno()] = \
|
|
|
|
+ (None, FakeReceiver((s, TEST_SERVER6, TEST_CLIENT6,
|
|
|
|
+ create_msg())))
|
|
|
|
+ self.assertEqual(expect_grant,
|
|
|
|
+ self.server.handle_session(s.fileno()))
|
|
|
|
+ self.assertEqual(0 if expect_grant else 1, s._close_called)
|
|
|
|
+
|
|
|
|
+ # By default up to 10 TCP clients can coexist (use hardcode
|
|
|
|
+ # intentionally so we can test the default value itself)
|
|
|
|
+ for i in range(0, 10):
|
|
|
|
+ check_tcp_ok(i, True)
|
|
|
|
+ self.assertEqual(10, len(self.server._tcp_ctxs))
|
|
|
|
+
|
|
|
|
+ # Beyond that, it should be rejected (by reset)
|
|
|
|
+ check_tcp_ok(11, False)
|
|
|
|
+
|
|
|
|
+ # If we remove one context from the server, new client can go in again.
|
|
|
|
+ self.server._tcp_ctxs.pop(5)
|
|
|
|
+ check_tcp_ok(12, True)
|
|
|
|
|
|
def test_request_message(self):
|
|
def test_request_message(self):
|
|
'''Test if the request message stores RRs separately.'''
|
|
'''Test if the request message stores RRs separately.'''
|
|
@@ -705,8 +974,124 @@ class TestDDNSession(unittest.TestCase):
|
|
num_rrsets = len(self.__req_message.get_section(SECTION_PREREQUISITE))
|
|
num_rrsets = len(self.__req_message.get_section(SECTION_PREREQUISITE))
|
|
self.assertEqual(2, num_rrsets)
|
|
self.assertEqual(2, num_rrsets)
|
|
|
|
|
|
|
|
+ def check_session_msg(self, result, expect_recv=1, notify_auth=False):
|
|
|
|
+ '''Check post update communication with other modules.'''
|
|
|
|
+ # iff the update succeeds, b10-ddns should tell interested other
|
|
|
|
+ # modules the information about the update zone. Possible modules
|
|
|
|
+ # are xfrout and auth: for xfrout, the message format should be:
|
|
|
|
+ # {'command': ['notify', {'zone_name': <updated_zone_name>,
|
|
|
|
+ # 'zone_class', <updated_zone_class>}]}
|
|
|
|
+ # for auth, it should be:
|
|
|
|
+ # {'command': ['loadzone', {'origin': <updated_zone_name>,
|
|
|
|
+ # 'class', <updated_zone_class>,
|
|
|
|
+ # 'datasrc', <datasrc type, should be
|
|
|
|
+ # "memory" in practice>}]}
|
|
|
|
+ # and expect an answer by calling group_recvmsg().
|
|
|
|
+ #
|
|
|
|
+ # expect_recv indicates the expected number of calls to
|
|
|
|
+ # group_recvmsg(), which is normally 1, but can be 0 if send fails;
|
|
|
|
+ # if the message is to be sent
|
|
|
|
+ if result == UPDATE_SUCCESS:
|
|
|
|
+ expected_sentmsg = 2 if notify_auth else 1
|
|
|
|
+ self.assertEqual(expected_sentmsg,
|
|
|
|
+ len(self.__cc_session._sent_msg))
|
|
|
|
+ self.assertEqual(expect_recv, self.__cc_session._recvmsg_called)
|
|
|
|
+ msg_cnt = 0
|
|
|
|
+ if notify_auth:
|
|
|
|
+ sent_msg, sent_group = self.__cc_session._sent_msg[msg_cnt]
|
|
|
|
+ sent_cmd = sent_msg['command']
|
|
|
|
+ self.assertEqual('Auth', sent_group)
|
|
|
|
+ self.assertEqual('loadzone', sent_cmd[0])
|
|
|
|
+ self.assertEqual(3, len(sent_cmd[1]))
|
|
|
|
+ self.assertEqual(TEST_ZONE_NAME.to_text(),
|
|
|
|
+ sent_cmd[1]['origin'])
|
|
|
|
+ self.assertEqual(TEST_RRCLASS.to_text(),
|
|
|
|
+ sent_cmd[1]['class'])
|
|
|
|
+ self.assertEqual('memory', sent_cmd[1]['datasrc'])
|
|
|
|
+ msg_cnt += 1
|
|
|
|
+ sent_msg, sent_group = self.__cc_session._sent_msg[msg_cnt]
|
|
|
|
+ sent_cmd = sent_msg['command']
|
|
|
|
+ self.assertEqual('Xfrout', sent_group)
|
|
|
|
+ self.assertEqual('notify', sent_cmd[0])
|
|
|
|
+ self.assertEqual(2, len(sent_cmd[1]))
|
|
|
|
+ self.assertEqual(TEST_ZONE_NAME.to_text(), sent_cmd[1]['zone_name'])
|
|
|
|
+ self.assertEqual(TEST_RRCLASS.to_text(), sent_cmd[1]['zone_class'])
|
|
|
|
+ else:
|
|
|
|
+ # for other result cases neither send nor recvmsg should be called.
|
|
|
|
+ self.assertEqual([], self.__cc_session._sent_msg)
|
|
|
|
+ self.assertEqual(0, self.__cc_session._recvmsg_called)
|
|
|
|
+
|
|
|
|
+ def test_session_msg(self):
|
|
|
|
+ '''Test post update communication with other modules.'''
|
|
|
|
+ # Normal cases, confirming communication takes place iff update
|
|
|
|
+ # succeeds
|
|
|
|
+ for r in [UPDATE_SUCCESS, UPDATE_ERROR, UPDATE_DROP]:
|
|
|
|
+ self.__cc_session.clear_msg()
|
|
|
|
+ self.check_session(result=r)
|
|
|
|
+ self.check_session_msg(r)
|
|
|
|
+
|
|
|
|
+ # Return an error from the remote module, which should be just ignored.
|
|
|
|
+ self.__cc_session.clear_msg()
|
|
|
|
+ self.__cc_session._answer_code = 1
|
|
|
|
+ self.check_session()
|
|
|
|
+ self.check_session_msg(UPDATE_SUCCESS)
|
|
|
|
+
|
|
|
|
+ # raise some exceptions from the faked session. Expected ones are
|
|
|
|
+ # simply (logged and) ignored
|
|
|
|
+ self.__cc_session.clear_msg()
|
|
|
|
+ self.__cc_session._recvmsg_exception = SessionTimeout('dummy timeout')
|
|
|
|
+ self.check_session()
|
|
|
|
+ self.check_session_msg(UPDATE_SUCCESS)
|
|
|
|
+
|
|
|
|
+ self.__cc_session.clear_msg()
|
|
|
|
+ self.__cc_session._recvmsg_exception = SessionError('dummy error')
|
|
|
|
+ self.check_session()
|
|
|
|
+ self.check_session_msg(UPDATE_SUCCESS)
|
|
|
|
+
|
|
|
|
+ self.__cc_session.clear_msg()
|
|
|
|
+ self.__cc_session._recvmsg_exception = ProtocolError('dummy perror')
|
|
|
|
+ self.check_session()
|
|
|
|
+ self.check_session_msg(UPDATE_SUCCESS)
|
|
|
|
+
|
|
|
|
+ # Similar to the previous cases, but sendmsg() raises, so there should
|
|
|
|
+ # be no call to recvmsg().
|
|
|
|
+ self.__cc_session.clear_msg()
|
|
|
|
+ self.__cc_session._sendmsg_exception = SessionError('send error')
|
|
|
|
+ self.check_session()
|
|
|
|
+ self.check_session_msg(UPDATE_SUCCESS, expect_recv=0)
|
|
|
|
+
|
|
|
|
+ # Unexpected exception will be propagated (and will terminate the
|
|
|
|
+ # server)
|
|
|
|
+ self.__cc_session.clear_msg()
|
|
|
|
+ self.__cc_session._sendmsg_exception = RuntimeError('unexpected')
|
|
|
|
+ self.assertRaises(RuntimeError, self.check_session)
|
|
|
|
+
|
|
|
|
+ def test_session_msg_for_auth(self):
|
|
|
|
+ '''Test post update communication with other modules including Auth.'''
|
|
|
|
+ # Let the CC session return in-memory config with sqlite3 backend.
|
|
|
|
+ # (The default case was covered by other tests.)
|
|
|
|
+ self.__cc_session.auth_datasources = \
|
|
|
|
+ [{'type': 'memory', 'class': 'IN', 'zones': [
|
|
|
|
+ {'origin': TEST_ZONE_NAME_STR, 'filetype': 'sqlite3'}]}]
|
|
|
|
+ self.check_session()
|
|
|
|
+ self.check_session_msg(UPDATE_SUCCESS, expect_recv=2, notify_auth=True)
|
|
|
|
+
|
|
|
|
+ # Let sendmsg() raise an exception. The first exception shouldn't
|
|
|
|
+ # stop sending the second message. There's just no recv calls.
|
|
|
|
+ self.__cc_session.clear_msg()
|
|
|
|
+ self.__cc_session._sendmsg_exception = SessionError('send error')
|
|
|
|
+ self.check_session()
|
|
|
|
+ self.check_session_msg(UPDATE_SUCCESS, expect_recv=0, notify_auth=True)
|
|
|
|
+
|
|
|
|
+ # Likewise, in the case recvmsg() raises (and there should be recv
|
|
|
|
+ # calls in this case)
|
|
|
|
+ self.__cc_session.clear_msg()
|
|
|
|
+ self.__cc_session._recvmsg_exception = SessionError('recv error')
|
|
|
|
+ self.check_session()
|
|
|
|
+ self.check_session_msg(UPDATE_SUCCESS, expect_recv=2, notify_auth=True)
|
|
|
|
+
|
|
def test_session_with_config(self):
|
|
def test_session_with_config(self):
|
|
- '''Check a session with more relistic config setups
|
|
|
|
|
|
+ '''Check a session with more realistic config setups.
|
|
|
|
|
|
We don't have to explore various cases in detail in this test.
|
|
We don't have to explore various cases in detail in this test.
|
|
We're just checking if the expected configured objects are passed
|
|
We're just checking if the expected configured objects are passed
|