Browse Source

[2013] added some basic update request processing in handle_request.

this is ongoing, with many TODO cases.
JINMEI Tatuya 13 years ago
parent
commit
7a70191fed
2 changed files with 136 additions and 3 deletions
  1. 44 3
      src/bin/ddns/ddns.py.in
  2. 92 0
      src/bin/ddns/tests/ddns_test.py

+ 44 - 3
src/bin/ddns/ddns.py.in

@@ -20,6 +20,7 @@ import sys; sys.path.append ('@@PYTHONPATH@@')
 import isc
 import isc
 import bind10_config
 import bind10_config
 from isc.dns import *
 from isc.dns import *
+import isc.ddns.session
 from isc.config.ccsession import *
 from isc.config.ccsession import *
 from isc.cc import SessionError, SessionTimeout
 from isc.cc import SessionError, SessionTimeout
 import isc.util.process
 import isc.util.process
@@ -120,6 +121,20 @@ class DDNSServer:
         self._listen_socket.bind(SOCKET_FILE)
         self._listen_socket.bind(SOCKET_FILE)
         self._listen_socket.listen(16)
         self._listen_socket.listen(16)
 
 
+        # DDNS Protocol handling class.  Parameterized for convenience of
+        # tests.  Essentially private (and constant), but for tests'
+        # convenience defined as "protected".
+        self._UpdateSessionClass = isc.ddns.session.UpdateSession
+
+    class SessionError(Exception):
+        '''Exception for internal errors in an update session.
+
+        This exception is expected to be caught within the server class,
+        only used for controling the code flow.
+
+        '''
+        pass
+
     def config_handler(self, new_config):
     def config_handler(self, new_config):
         '''Update config data.'''
         '''Update config data.'''
         # TODO: Handle exceptions and turn them to an error response
         # TODO: Handle exceptions and turn them to an error response
@@ -180,7 +195,7 @@ class DDNSServer:
             # continue with the rest
             # continue with the rest
             logger.error(DDNS_ACCEPT_FAILURE, e)
             logger.error(DDNS_ACCEPT_FAILURE, e)
 
 
-    def handle_request(self, request):
+    def handle_request(self, req_session):
         """
         """
         This is the place where the actual DDNS processing is done. Other
         This is the place where the actual DDNS processing is done. Other
         methods are either subroutines of this method or methods doing the
         methods are either subroutines of this method or methods doing the
@@ -190,12 +205,38 @@ class DDNSServer:
         It is called with the request being session as received from
         It is called with the request being session as received from
         SocketSessionReceiver, i.e. tuple
         SocketSessionReceiver, i.e. tuple
         (socket, local_address, remote_address, data).
         (socket, local_address, remote_address, data).
+
         """
         """
-        # TODO: Implement the magic
+        # give tuple elements intuitive names
+        (sock, local_addr, remote_addr, req_data) = req_session
+
+        # The session sender (b10-auth) should have made sure that this is
+        # a validly formed DNS message of OPCODE being UPDATE, and if it's
+        # TSIG signed, its key is known to the system and the signature is
+        # valid.  Messages that don't meet these should have been resopnded
+        # or dropped by the sender, so if such error is detected we treat it
+        # as an internal error and don't bother to respond.
+        try:
+            msg = Message(Message.PARSE)
+            msg.from_wire(req_data)
+            if msg.get_opcode() != Opcode.UPDATE():
+                raise SessionError('Update request has unexpected opcode: ' +
+                                   str(msg.get_opcode()))
+            # TODO: TSIG check
+        except Exception as ex:
+            return False
 
 
         # TODO: Don't propagate most of the exceptions (like datasrc errors),
         # TODO: Don't propagate most of the exceptions (like datasrc errors),
         # just drop the packet.
         # just drop the packet.
-        pass
+
+        update_session = self._UpdateSessionClass(msg, remote_addr, None)
+        result, zname, zclass = update_session.handle()
+        msg = update_session.get_message()
+        renderer = MessageRenderer()
+        msg.to_wire(renderer)
+        sock.sendto(renderer.get_data(), remote_addr)
+
+        return True
 
 
     def handle_session(self, fileno):
     def handle_session(self, fileno):
         """
         """

+ 92 - 0
src/bin/ddns/tests/ddns_test.py

@@ -17,6 +17,7 @@
 
 
 import unittest
 import unittest
 import isc
 import isc
+from isc.ddns.session import *
 import ddns
 import ddns
 import isc.config
 import isc.config
 import select
 import select
@@ -24,6 +25,16 @@ import errno
 import isc.util.cio.socketsession
 import isc.util.cio.socketsession
 import socket
 import socket
 import os.path
 import os.path
+from isc.dns import *
+
+# Some common test parameters
+TEST_ZONE_NAME = Name('example.org')
+UPDATE_RRTYPE = RRType.SOA()
+TEST_QID = 5353                 # arbitrary chosen
+TEST_RRCLASS = RRClass.IN()
+TEST_SERVER6 = ('2001:db8::53', 53, 0, 0)
+TEST_CLIENT6 = ('2001:db8::1', 53000, 0, 0)
+TEST_ZONE_RECORD = Question(TEST_ZONE_NAME, TEST_RRCLASS, UPDATE_RRTYPE)
 
 
 class FakeSocket:
 class FakeSocket:
     """
     """
@@ -31,12 +42,17 @@ class FakeSocket:
     """
     """
     def __init__(self, fileno):
     def __init__(self, fileno):
         self.__fileno = fileno
         self.__fileno = fileno
+        self._sent_data = None
+        self._sent_addr = None
     def fileno(self):
     def fileno(self):
         return self.__fileno
         return self.__fileno
     def getpeername(self):
     def getpeername(self):
         return "fake_unix_socket"
         return "fake_unix_socket"
     def accept(self):
     def accept(self):
         return FakeSocket(self.__fileno + 1)
         return FakeSocket(self.__fileno + 1)
+    def sendto(self, data, addr):
+        self._sent_data = data
+        self._sent_addr = addr
 
 
 class FakeSessionReceiver:
 class FakeSessionReceiver:
     """
     """
@@ -51,6 +67,19 @@ class FakeSessionReceiver:
         """
         """
         return self._socket
         return self._socket
 
 
+class FakeUpdateSession:
+    def __init__(self, msg, client_addr, zone_config):
+        self.__msg = msg
+
+    def handle(self):
+        return UPDATE_SUCCESS, TEST_ZONE_NAME, TEST_RRCLASS
+
+    def get_message(self):
+        self.__msg.make_response()
+        self.__msg.clear_section(SECTION_ZONE)
+        self.__msg.set_rcode(Rcode.NOERROR())
+        return self.__msg
+
 class MyCCSession(isc.config.ConfigData):
 class MyCCSession(isc.config.ConfigData):
     '''Fake session with minimal interface compliance'''
     '''Fake session with minimal interface compliance'''
     def __init__(self):
     def __init__(self):
@@ -361,6 +390,69 @@ 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 create_msg(opcode=Opcode.UPDATE(), zones=[TEST_ZONE_RECORD], tsig_key=None):
+    msg = Message(Message.RENDER)
+    msg.set_qid(TEST_QID)
+    msg.set_opcode(opcode)
+    msg.set_rcode(Rcode.NOERROR())
+    for z in zones:
+        msg.add_question(z)
+
+    renderer = MessageRenderer()
+    if tsig_key is not None:
+        msg.to_wire(renderer, TSIGContext(tsig_key))
+    else:
+        msg.to_wire(renderer)
+
+    # re-read the created data in the parse mode
+    msg.clear(Message.PARSE)
+    msg.from_wire(renderer.get_data())
+
+    return renderer.get_data()
+
+
+class TestDDNSession(unittest.TestCase):
+    def setUp(self):
+        cc_session = MyCCSession()
+        self.assertFalse(cc_session._started)
+        self.server = ddns.DDNSServer(cc_session)
+        self.server._UpdateSessionClass = FakeUpdateSession
+        self.__sock = FakeSocket(-1)
+
+    def check_update_response(self, resp_wire, expected_rcode=Rcode.NOERROR()):
+        '''Check if given wire data are valid form of update response.
+
+        In this implementation, zone/prerequisite/update sections should be
+        empty in responses.
+
+        '''
+        msg = Message(Message.PARSE)
+        msg.from_wire(resp_wire)
+        self.assertEqual(Opcode.UPDATE(), msg.get_opcode())
+        self.assertEqual(expected_rcode, msg.get_rcode())
+        self.assertEqual(TEST_QID, msg.get_qid())
+        for section in [SECTION_ZONE, SECTION_PREREQUISITE, SECTION_UPDATE]:
+            self.assertEqual(0, msg.get_rr_count(section))
+
+    def test_handle_request(self):
+        self.assertTrue(self.server.handle_request((self.__sock,
+                                                    TEST_SERVER6, TEST_CLIENT6,
+                                                    create_msg())))
+        self.assertEqual(TEST_CLIENT6, self.__sock._sent_addr)
+        self.check_update_response(self.__sock._sent_data)
+
+    def test_broken_request(self):
+        # Message data too short
+        s = self.__sock
+        self.assertFalse(self.server.handle_request((self.__sock, None,
+                                                     None, b'x' * 11)))
+        self.assertEqual((None, None), (s._sent_data, s._sent_addr))
+
+        # Opcode is not UPDATE
+        self.assertFalse(self.server.handle_request(
+                (self.__sock, None, None, create_msg(opcode=Opcode.QUERY()))))
+        self.assertEqual((None, None), (s._sent_data, s._sent_addr))
+
 class TestMain(unittest.TestCase):
 class TestMain(unittest.TestCase):
     def setUp(self):
     def setUp(self):
         self._server = MyDDNSServer()
         self._server = MyDDNSServer()