Browse Source

[trac800] The parser

Michal 'vorner' Vaner 13 years ago
parent
commit
517c31a58a
3 changed files with 160 additions and 7 deletions
  1. 78 3
      src/bin/bind10/sockcreator.py
  2. 81 3
      src/bin/bind10/tests/sockcreator_test.py
  3. 1 1
      src/bin/sockcreator/README

+ 78 - 3
src/bin/bind10/sockcreator.py

@@ -13,6 +13,9 @@
 # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
 # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 
+import socket
+import struct
+
 """
 Module that comunicates with the priviledget socket creator (b10-sockcreator).
 """
@@ -55,14 +58,26 @@ class Parser:
         have a read_fd() method to read the file descriptor. This slightly
         unusual modification of socket object is used to easy up testing.
         """
-        pass # TODO Implement
+        self.__socket = creator_socket
 
     def terminate(self):
         """
         Asks the creator process to terminate and waits for it to close the
         socket. Does not return anything.
         """
-        pass # TODO Implement
+        if self.__socket is None:
+            raise CreatorError('Terminated already', True)
+        try:
+            self.__socket.sendall(b'T')
+            # Wait for an EOF - it will return empty data
+            eof = self.__socket.recv(1)
+            if len(eof) != 0:
+                raise CreatorError('Protocol error - data after terminated',
+                                   True)
+            self.__socket = None
+        except socket.error as se:
+            self.__socket = None
+            raise CreatorError(str(se), True)
 
     def get_socket(self, address, port, socktype):
         """
@@ -75,4 +90,64 @@ class Parser:
         should be fast, as it is on localhost) and returns the file descriptor
         number. It raises a CreatorError exception if the creation fails.
         """
-        pass # TODO Implement
+        if self.__socket is None:
+            raise CreatorError('Socket requested on terminated creator', True)
+        # First, assemble the request from parts
+        data = b'S'
+        if socktype == 'UDP' or socktype == socket.SOCK_DGRAM:
+            data += b'U'
+        elif socktype == 'TCP' or socktype == socket.SOCK_STREAM:
+            data += b'T'
+        else:
+            raise ValueError('Unknown socket type: ' + str(socktype))
+        if address.family == socket.AF_INET:
+            data += b'4'
+        elif address.family == socket.AF_INET6:
+            data += b'6'
+        else:
+            raise ValueError('Unknown address family in address')
+        data += struct.pack('!H', port)
+        data += address.addr
+        try:
+            # Send the request
+            self.__socket.sendall(data)
+            answer = self.__socket.recv(1)
+            if answer == b'S':
+                # Success!
+                return self.__socket.read_fd()
+            elif answer == b'E':
+                # There was an error, read the error as well
+                error = self.__socket.recv(1)
+                errno = struct.unpack('i',
+                                      self.__read_all(len(struct.pack('i',
+                                                                      0))))
+                if error == b'S':
+                    cause = 'socket'
+                elif error == b'B':
+                    cause = 'bind'
+                else:
+                    self.__socket = None
+                    raise CreatorError('Unknown error cause' + str(answer), True)
+                raise CreatorError('Error creating socket on ' + cause, False,
+                                   errno[0])
+            else:
+                self.__socket = None
+                raise CreatorError('Unknown response ' + str(answer), True)
+        except socket.error as se:
+            self.__socket = None
+            raise CreatorError(str(se), True)
+
+    def __read_all(self, length):
+        """
+        Keeps reading until length data is read or EOF or error happens.
+
+        EOF is considered error as well and throws.
+        """
+        result = b''
+        while len(result) < length:
+            data = self.__socket.recv(length - len(result))
+            if len(data) == 0:
+                self.__socket = None
+                raise CreatorError('Unexpected EOF', True)
+            result += data
+        return result

+ 81 - 3
src/bin/bind10/tests/sockcreator_test.py

@@ -61,6 +61,7 @@ class FakeCreator:
             raise InvalidPlan('Nothing more planned')
         (kind, data) = self.__plan[0]
         if kind == 'e':
+            self.__plan.pop(0)
             raise socket.error('False socket error')
         if kind != expected:
             raise InvalidPlan('Planned ' + kind + ', but ' + expected +
@@ -108,6 +109,7 @@ class FakeCreator:
             self.__plan[0] = ('s', rest)
         else:
             self.__plan.pop(0)
+
     def all_used(self):
         """
         Returns if the whole plan was consumed.
@@ -118,15 +120,65 @@ class ParserTests(unittest.TestCase):
     """
     Testcases for the Parser class.
     """
+    def __terminate(self):
+        creator = FakeCreator([('s', b'T'), ('r', b'')])
+        parser = Parser(creator)
+        self.assertEqual(None, parser.terminate())
+        self.assertTrue(creator.all_used())
+        return parser
+
     def test_terminate(self):
         """
         Test if the command to terminate is correct and it waits for reading the
         EOF.
         """
-        creator = FakeCreator([('s', b'T'), ('r', b'')])
+        self.__terminate()
+
+    def test_terminate_error1(self):
+        """
+        Test it reports an exception when there's error terminating the creator.
+        This one raises an error when receiving the EOF.
+        """
+        creator = FakeCreator([('s', b'T'), ('e', None)])
         parser = Parser(creator)
-        self.assertEqual(None, parser.terminate())
-        self.assertTrue(creator.all_used())
+        with self.assertRaises(CreatorError) as cm:
+            parser.terminate()
+        self.assertTrue(cm.exception.fatal)
+        self.assertEqual(None, cm.exception.errno)
+
+    def test_terminate_error2(self):
+        """
+        Test it reports an exception when there's error terminating the creator.
+        This one raises an error when sending data.
+        """
+        creator = FakeCreator([('e', None)])
+        parser = Parser(creator)
+        with self.assertRaises(CreatorError) as cm:
+            parser.terminate()
+        self.assertTrue(cm.exception.fatal)
+        self.assertEqual(None, cm.exception.errno)
+
+    def test_terminate_twice(self):
+        """
+        Test we can't terminate twice.
+        """
+        parser = self.__terminate()
+        with self.assertRaises(CreatorError) as cm:
+            parser.terminate()
+        self.assertTrue(cm.exception.fatal)
+        self.assertEqual(None, cm.exception.errno)
+
+    def test_terminate_error3(self):
+        """
+        Test it reports an exception when there's error terminating the creator.
+        This one sends data when it should have terminated.
+        """
+        creator = FakeCreator([('s', b'T'), ('r', b'Extra data')])
+        parser = Parser(creator)
+        with self.assertRaises(CreatorError) as cm:
+            parser.terminate()
+        self.assertTrue(cm.exception.fatal)
+        self.assertEqual(None, cm.exception.errno)
 
     def test_crash(self):
         """
@@ -189,5 +241,31 @@ class ParserTests(unittest.TestCase):
         self.__create('2001:db8::', socket.SOCK_STREAM,
             b'T6\0\x2A\x20\x01\x0d\xb8\0\0\0\0\0\0\0\0\0\0\0\0')
 
+    def test_create_terminated(self):
+        """
+        Test we can't request sockets after it was terminated.
+        """
+        parser = self.__terminate()
+        with self.assertRaises(CreatorError) as cm:
+            parser.get_socket(IPAddr('0.0.0.0'), 0, 'UDP')
+        self.assertTrue(cm.exception.fatal)
+        self.assertEqual(None, cm.exception.errno)
+
+    def test_invalid_socktype(self):
+        """
+        Test invalid socket type is rejected
+        """
+        self.assertRaises(ValueError, Parser(FakeCreator([])).get_socket,
+                          IPAddr('0.0.0.0'), 42, 'RAW')
+
+    def test_invalid_family(self):
+        """
+        Test it rejects invalid address family.
+        """
+        addr = IPAddr('0.0.0.0')
+        addr.family = 'Nonsense'
+        self.assertRaises(ValueError, Parser(FakeCreator([])).get_socket,
+                          addr, 42, socket.SOCK_DGRAM)
+
 if __name__ == '__main__':
     unittest.main()

+ 1 - 1
src/bin/sockcreator/README

@@ -3,7 +3,7 @@ The socket creator
 
 The only thing we need higher rights than standard user is binding sockets to
 ports lower than 1024. So we will have a separate process that keeps the
-rights, while the rests drop them for security reasons.
+rights, while the rest drops them for security reasons.
 
 This process is the socket creator. Its goal is to be as simple as possible
 and to contain as little code as possible to minimise the amount of code