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 bind10_config
 from isc.dns import *
+import isc.ddns.session
 from isc.config.ccsession import *
 from isc.cc import SessionError, SessionTimeout
 import isc.util.process
@@ -120,6 +121,20 @@ class DDNSServer:
         self._listen_socket.bind(SOCKET_FILE)
         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):
         '''Update config data.'''
         # TODO: Handle exceptions and turn them to an error response
@@ -180,7 +195,7 @@ class DDNSServer:
             # continue with the rest
             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
         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
         SocketSessionReceiver, i.e. tuple
         (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),
         # 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):
         """

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

@@ -17,6 +17,7 @@
 
 import unittest
 import isc
+from isc.ddns.session import *
 import ddns
 import isc.config
 import select
@@ -24,6 +25,16 @@ import errno
 import isc.util.cio.socketsession
 import socket
 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:
     """
@@ -31,12 +42,17 @@ class FakeSocket:
     """
     def __init__(self, fileno):
         self.__fileno = fileno
+        self._sent_data = None
+        self._sent_addr = None
     def fileno(self):
         return self.__fileno
     def getpeername(self):
         return "fake_unix_socket"
     def accept(self):
         return FakeSocket(self.__fileno + 1)
+    def sendto(self, data, addr):
+        self._sent_data = data
+        self._sent_addr = addr
 
 class FakeSessionReceiver:
     """
@@ -51,6 +67,19 @@ class FakeSessionReceiver:
         """
         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):
     '''Fake session with minimal interface compliance'''
     def __init__(self):
@@ -361,6 +390,69 @@ class TestDDNSServer(unittest.TestCase):
         self.__select_expected = ([1, 2], [], [], None)
         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):
     def setUp(self):
         self._server = MyDDNSServer()