[Pkg-privacy-commits] [obfsproxy] 94/353: Implement prototype of an integration test framework.

Ximin Luo infinity0 at moszumanska.debian.org
Sat Aug 22 13:01:44 UTC 2015


This is an automated email from the git hooks/post-receive script.

infinity0 pushed a commit to branch master
in repository obfsproxy.

commit 5f2b870b60250da4ae2afae446c5dcf484b21946
Author: George Kadianakis <desnacked at riseup.net>
Date:   Thu Dec 13 12:06:20 2012 +0200

    Implement prototype of an integration test framework.
---
 obfsproxy/network/network.py                   |   2 +-
 obfsproxy/test/int_tests/obfsproxy_tester.py   |  88 ++++++++++
 obfsproxy/test/int_tests/pits.py               | 225 +++++++++++++++++++++++++
 obfsproxy/test/int_tests/pits_connections.py   | 109 ++++++++++++
 obfsproxy/test/int_tests/pits_design.txt       | 149 ++++++++++++++++
 obfsproxy/test/int_tests/pits_network.py       | 168 ++++++++++++++++++
 obfsproxy/test/int_tests/pits_transcript.py    |  98 +++++++++++
 obfsproxy/test/int_tests/test_case.pits        |  16 ++
 obfsproxy/test/int_tests/test_case_simple.pits |   6 +
 obfsproxy/test/int_tests/test_pits.py          |  43 +++++
 10 files changed, 903 insertions(+), 1 deletion(-)

diff --git a/obfsproxy/network/network.py b/obfsproxy/network/network.py
index 4cb95ca..8c2a25f 100644
--- a/obfsproxy/network/network.py
+++ b/obfsproxy/network/network.py
@@ -146,7 +146,7 @@ class Circuit(Protocol):
                 log.debug("%s: upstream: Received %d bytes." % (self.name, len(data)))
                 self.transport.receivedUpstream(data, self)
         except base.PluggableTransportError, err: # Our transport didn't like that data.
-            log.debug("%s: %s: Closing circuit." % (self.name, str(err)))
+            log.info("%s: %s: Closing circuit." % (self.name, str(err)))
             self.close()
 
     def close(self, reason=None, side=None):
diff --git a/obfsproxy/test/int_tests/obfsproxy_tester.py b/obfsproxy/test/int_tests/obfsproxy_tester.py
new file mode 100644
index 0000000..343f69e
--- /dev/null
+++ b/obfsproxy/test/int_tests/obfsproxy_tester.py
@@ -0,0 +1,88 @@
+#!/usr/bin/python
+
+from twisted.internet import reactor
+from twisted.internet import protocol
+
+import os
+import logging
+import subprocess
+import threading
+
+obfsproxy_env = {}
+obfsproxy_env.update(os.environ)
+
+class ObfsproxyProcess(protocol.ProcessProtocol):
+    """
+    Represents the behavior of an obfsproxy process.
+    """
+    def __init__(self):
+        self.stdout_data = ''
+        self.stderr_data = ''
+
+        self.name = 'obfs_%s' % hex(id(self))
+
+    def connectionMade(self):
+        pass
+
+    def outReceived(self, data):
+        """Got data in stdout."""
+        logging.debug('%s: outReceived got %d bytes of data.' % (self.name, len(data)))
+        self.stdout_data += data
+
+    def errReceived(self, data):
+        """Got data in stderr."""
+        logging.debug('%s: errReceived got %d bytes of data.' % (self.name, len(data)))
+        self.stderr_data += data
+
+    def inConnectionLost(self):
+        """stdin closed."""
+        logging.debug('%s: stdin closed' % self.name)
+
+    def outConnectionLost(self):
+        """stdout closed."""
+        logging.debug('%s: outConnectionLost, stdout closed!' % self.name)
+        # XXX Fail the test if stdout is not clean.
+        if self.stdout_data != '':
+            logging.warning('%s: stdout is not clean: %s' % (self.name, self.stdout_data))
+
+    def errConnectionLost(self):
+        """stderr closed."""
+        logging.debug('%s: errConnectionLost, stderr closed!' % self.name)
+
+    def processExited(self, reason):
+        """Process exited."""
+        logging.debug('%s: processExited, status %s' % (self.name, str(reason.value.exitCode)))
+
+    def processEnded(self, reason):
+        """Process ended."""
+        logging.debug('%s: processEnded, status %s' % (self.name, str(reason.value.exitCode)))
+
+    def kill(self):
+        """Kill the process."""
+        logging.debug('%s: killing' % self.name)
+        self.transport.signalProcess('KILL')
+        self.transport.loseConnection()
+
+class Obfsproxy(object):
+    def __init__(self, *args, **kwargs):
+        # Fix up our argv
+        argv = []
+        argv.extend(('python', '../../../../obfsproxy.py', '--log-min-severity=warning'))
+
+        # Extend hardcoded argv with user-specified options.
+        if len(args) == 1 and (isinstance(args[0], list) or
+                               isinstance(args[0], tuple)):
+            argv.extend(args[0])
+        else:
+            argv.extend(args)
+
+        # Launch obfsproxy
+        self.obfs_process = ObfsproxyProcess()
+        reactor.spawnProcess(self.obfs_process, 'python', args=argv,
+                             env=obfsproxy_env)
+
+        logging.debug('spawnProcess with %s' % str(argv))
+
+    def kill(self):
+        """Kill the obfsproxy process."""
+        self.obfs_process.kill()
diff --git a/obfsproxy/test/int_tests/pits.py b/obfsproxy/test/int_tests/pits.py
new file mode 100644
index 0000000..4cdb3cd
--- /dev/null
+++ b/obfsproxy/test/int_tests/pits.py
@@ -0,0 +1,225 @@
+#!/usr/bin/python
+
+import sys
+import logging
+import time
+import socket
+import collections
+
+from twisted.internet import task, reactor, defer
+
+import pits_network as network
+import pits_connections as conns
+import obfsproxy_tester
+import pits_transcript as transcript
+
+def usage():
+    print "PITS usage:\n\tpits.py test_case.pits"
+
+CLIENT_OBFSPORT = 42000 # XXX maybe randomize?
+SERVER_OBFSPORT = 62000
+
+class PITS(object):
+    """
+    The PITS system. It executes the commands written in test case
+    files.
+
+    Attributes:
+    'transcript', is the PITS transcript. It's being written while
+    the tests are running.
+
+    'inbound_listener', the inbound PITS listener.
+
+    'client_obfs' and 'server_obfs', the client and server obfpsroxy processes.
+    """
+
+    def __init__(self):
+        # Set up the transcript
+        self.transcript = transcript.Transcript()
+
+        # Set up connection handler
+        self.conn_handler = conns.PITSConnectionHandler()
+
+        # Set up our fake network:
+        # <PITS OUTBOUND CONNECTION> -> CLIENT_OBFSPORT -> SERVER_OBFSPRORT -> <PITS inbound listener>
+
+        # Set up PITS inbound listener
+        self.inbound_factory = network.PITSInboundFactory(self.transcript, self.conn_handler)
+        self.inbound_listener = reactor.listenTCP(0, self.inbound_factory, interface='localhost')
+
+        self.client_obfs = None
+        self.server_obfs = None
+
+        logging.debug("PITS initialized.")
+
+    def get_pits_inbound_address(self):
+        """Return the address of the PITS inbound listener."""
+        return self.inbound_listener.getHost()
+
+    def launch_obfsproxies(self, obfs_client_args, obfs_server_args):
+        """Launch client and server obfsproxies with the given cli arguments."""
+        # Set up client Obfsproxy.
+        self.client_obfs = obfsproxy_tester.Obfsproxy(*obfs_client_args)
+
+        # Set up server Obfsproxy.
+        self.server_obfs = obfsproxy_tester.Obfsproxy(*obfs_server_args)
+
+        time.sleep(1)
+
+    def pause(self, tokens):
+        """Read a parse command."""
+        if len(tokens) > 1:
+            raise InvalidCommand("Too big pause line.")
+
+        if not tokens[0].isdigit():
+            raise InvalidCommand("Invalid pause argument (%s)." % tokens[0])
+
+        time.sleep(int(tokens[0]))
+
+    def init_conn(self, tokens):
+        """Read a connection establishment command."""
+        if len(tokens) > 1:
+            raise InvalidCommand("Too big init connection line.")
+
+        # Before firing up the connetion, register its identifier to
+        # the PITS subsystem.
+        self.inbound_factory.register_new_identifier(tokens[0])
+
+        # Create outbound socket. tokens[0] is its identifier.
+        factory = network.PITSOutboundFactory(tokens[0], self.transcript, self.conn_handler)
+        reactor.connectTCP('127.0.0.1', CLIENT_OBFSPORT, factory)
+
+    def transmit(self, tokens, direction):
+        """Read a transmit command."""
+        if len(tokens) < 2:
+            raise InvalidCommand("Too small transmit line.")
+
+        identifier = tokens[0]
+        data = " ".join(tokens[1:]) # concatenate rest of the line
+        data = data.decode('string_escape') # unescape string
+
+        try:
+            self.conn_handler.send_data_through_conn(identifier, direction, data)
+        except conns.NoSuchConn, err:
+            logging.warning("Wanted to send some data, but I can't find '%s' connection with id '%s'." % \
+                                (direction, identifier))
+            # XXX note it to transcript
+
+        logging.debug("Sending '%s' from '%s' socket '%s'." % (data, direction, identifier))
+
+    def eof(self, tokens, direction):
+        """Read a transmit EOF command."""
+        if len(tokens) > 1:
+            raise InvalidCommand("Too big EOF line.")
+
+        identifier = tokens[0]
+
+        try:
+            self.conn_handler.close_conn(identifier, direction)
+        except conns.NoSuchConn, err:
+            logging.warning("Wanted to EOF, but I can't find '%s' connection with id '%s'." % \
+                                (direction, identifier))
+            # XXX note it to transcript
+
+        logging.debug("Sending EOF from '%s' socket '%s'." % (identifier, direction))
+
+    def do_command(self, line):
+        """
+        Parse command from 'line'.
+
+        Throws InvalidCommand.
+        """
+        logging.debug("Parsing %s" % repr(line))
+
+        line = line.rstrip()
+
+        if line == '': # Ignore blank lines
+            return
+
+        tokens = line.split(" ")
+
+        if len(tokens) < 2:
+            raise InvalidCommand("Too few tokens: '%s'." % line)
+
+        if tokens[0] == 'P':
+            self.pause(tokens[1:])
+        elif tokens[0] == '!':
+            self.init_conn(tokens[1:])
+        elif tokens[0] == '>':
+            self.transmit(tokens[1:], 'outbound')
+        elif tokens[0] == '<':
+            self.transmit(tokens[1:], 'inbound')
+        elif tokens[0] == '*':
+            self.eof(tokens[1:], 'inbound')
+        elif tokens[0] == '#': # comment
+            pass
+        else:
+            logging.warning("Unknown token in line: '%s'" % line)
+
+    def cleanup(self):
+        logging.debug("Cleanup.")
+        self.inbound_listener.stopListening()
+        self.client_obfs.kill()
+        self.server_obfs.kill()
+
+class TestReader(object):
+    """
+    Read and execute a test case from a file.
+
+    Attributes:
+    'script', is the text of the test case file.
+    'test_case_line', is a generator that yields the next line of the test case file.
+    'pits', is the PITS system responsible for this test case.
+    'assertTrue', is a function pointer to a unittest.assertTrue
+                  function that should be used to validate this test.
+    """
+    def __init__(self, test_assertTrue_func, fname):
+        self.assertTrue = test_assertTrue_func
+
+        self.script = open(fname).read()
+        self.test_case_line = self.test_case_line_gen()
+
+        self.pits = PITS()
+
+    def test_case_line_gen(self):
+        """Yield the next line of the test case file."""
+        for line in self.script.split('\n'):
+            yield line
+
+    def do_test(self, obfs_client_args, obfs_server_args):
+        """
+        Start a test case with obfsproxies with the given arguments.
+        """
+
+        # Launch the obfsproxies
+        self.pits.launch_obfsproxies(obfs_client_args, obfs_server_args)
+
+        # We call _do_command() till we read the whole test case
+        # file. After we read the file, we call
+        # transcript.test_was_success() to verify the test run.
+        d = task.deferLater(reactor, 0.2, self._do_command)
+        return d
+
+    def _do_command(self):
+        """
+        Read and execute another command from the test case file.
+        If the test case file is over, verify that the test was succesful. 
+        """
+
+        try:
+            line = self.test_case_line.next()
+        except StopIteration: # Test case is over.
+            return self.assertTrue(self.pits.transcript.test_was_success(self.script))
+
+        self.pits.do_command(line)
+
+        # 0.2 seconds should be enough time for the network operations to complete,
+        # so that we can move to the next command.
+        d = task.deferLater(reactor, 0.2, self._do_command)
+        return d
+
+    def cleanup(self):
+        self.pits.cleanup()
+
+class InvalidCommand(Exception): pass
+
diff --git a/obfsproxy/test/int_tests/pits_connections.py b/obfsproxy/test/int_tests/pits_connections.py
new file mode 100644
index 0000000..77b5062
--- /dev/null
+++ b/obfsproxy/test/int_tests/pits_connections.py
@@ -0,0 +1,109 @@
+import logging
+
+"""
+Code that keeps track of the connections of PITS.
+"""
+
+def remove_key(d, key):
+    """
+    Return a dictionary identical to 'd' but with 'key' (and its
+    value) removed.
+    """
+    r = dict(d)
+    del r[key]
+    return r
+
+class PITSConnectionHandler(object):
+    """
+    Responsible for managing PITS connections.
+
+    Attributes:
+    'active_outbound_conns', is a dictionary mapping outbound connection identifiers with their objects.
+    'active_inbound_conns', is a dictionary mapping inbound connection identifiers with their objects.
+    """
+
+    def __init__(self):
+        # { "id1" : <OutboundConnection #1>, "id2": <OutboundConnection #2> }
+        self.active_outbound_conns = {}
+
+        # { "id1" : <InboundConnection #1>, "id2": <InboundConnection #2> }
+        self.active_inbound_conns = {}
+
+    def register_conn(self, conn, identifier, direction):
+        """
+        Register connection 'conn' with 'identifier'. 'direction' is
+        either "inbound" or "outbound".
+        """
+
+        if direction == 'inbound':
+            self.active_inbound_conns[identifier] = conn
+            logging.debug("active_inbound_conns: %s" % str(self.active_inbound_conns))
+        elif direction == 'outbound':
+            self.active_outbound_conns[identifier] = conn
+            logging.debug("active_outbound_conns: %s" % str(self.active_outbound_conns))
+
+    def unregister_conn(self, identifier, direction):
+        """
+        Unregister connection 'conn' with 'identifier'. 'direction' is
+        either "inbound" or "outbound".
+        """
+
+        if direction == 'inbound':
+            self.active_inbound_conns = remove_key(self.active_inbound_conns, identifier)
+            logging.debug("active_inbound_conns: %s" % str(self.active_inbound_conns))
+        elif direction == 'outbound':
+            self.active_outbound_conns = remove_key(self.active_outbound_conns, identifier)
+            logging.debug("active_outbound_conns: %s" % str(self.active_outbound_conns))
+
+    def find_conn(self, identifier, direction):
+        """
+        Find connection with 'identifier'. 'direction' is either
+        "inbound" or "outbound".
+
+        Raises NoSuchConn.
+        """
+
+        conn = None
+
+        try:
+            if direction == 'inbound':
+                conn = self.active_inbound_conns[identifier]
+            elif direction == 'outbound':
+                conn = self.active_outbound_conns[identifier]
+        except KeyError:
+            logging.warning("find_conn: Could not find '%s' connection with identifier '%s'" %
+                            (direction, identifier))
+            raise NoSuchConn()
+
+        logging.debug("Found '%s' conn with identifier '%s': '%s'" % (direction, identifier, conn))
+        return conn
+
+    def send_data_through_conn(self, identifier, direction, data):
+        """
+        Send 'data' through connection with 'identifier'.
+        """
+
+        try:
+            conn = self.find_conn(identifier, direction)
+        except KeyError:
+            logging.warning("send_data_through_conn: Could not find '%s' connection "
+                            "with identifier '%s'" % (direction, identifier))
+            raise NoSuchConn()
+
+        conn.write(data)
+
+    def close_conn(self, identifier, direction):
+        """
+        Send EOF through connection with 'identifier'.
+        """
+
+        try:
+            conn = self.find_conn(identifier, direction)
+        except KeyError:
+            logging.warning("close_conn: Could not find '%s' connection "
+                            "with identifier '%s'" % (direction, identifier))
+            raise NoSuchConn()
+
+        conn.close()
+
+class NoSuchConn(Exception): pass
diff --git a/obfsproxy/test/int_tests/pits_design.txt b/obfsproxy/test/int_tests/pits_design.txt
new file mode 100644
index 0000000..a627e1a
--- /dev/null
+++ b/obfsproxy/test/int_tests/pits_design.txt
@@ -0,0 +1,149 @@
+Pyobfsproxy integration test suite (PITS)
+
+Overview
+
+  Obfsproxy needs an automated and robust way of testing its pluggable
+  transports. While unit tests are certainly helpful, integration
+  tests provide realistic testing scenarios for network daemons like
+  obfsproxy.
+
+Motivation
+
+  Obfsproxy needs to be tested on how well it can proxy traffic from
+  one side to its other side. A basic integration test would be to
+  transfer a string from one side and see if it arrives intact on the
+  other side.
+
+  A more involved integration test is the "timeline tests" of
+  Stegotorus, developed by Zack Weinberg. Stegotorus integration tests
+  are configurable: you pass them a script file that defines the
+  behavior of the integration test connections. This allows
+  customizable connection establishment and tear down, and the ability
+  to send arbitrary data through the integration test connections.
+
+  That's good enough, but sometimes bugs appear on more complex
+  network interactions. For this reason, PITS was developed which has
+  support for:
+    + multiple network connections
+    + flexible connection behavior
+    + automated test case generation
+
+  The integration tests should also be cross-platform so that they can
+  be ran on Microsoft Windows.
+
+Design
+
+
+
+                  +-----------+                      +-----------+
+        |-------->| client    |<-------------------->| server    |<--------|
+        |  |----->| obfsproxy |<-------------------->| obfsproxy |<-----|  |
+        |  |  |-->|           |<-------------------->|           |<--|  |  |
+        |  |  |   +-----------+                      +-----------+   |  |  |
+        |  |  |                                                      |  |  |
+        v  v  v                                                      v  v  v
+   +---------------+                                            +---------------+
+   | PITS outbound |                                            | PITS inbound  |
+   +---------------+                                            +---------------+
+           ^                                                            |
+           |                                                            |
+           |                                                            v
+   +---------------+                                            +---------------+
+   |Test case file |<----------------<validation>-------------->|Transcript file|
+   +---------------+                                            +---------------+
+
+  PITS does integration tests by reading a user-provided test case
+  file which contains a description of the test that PITS should
+  perform.
+
+  A basic PITS test case usually involves launching two obfsproxies as
+  in the typical obfuscated bridge client-server scenario, exchanging
+  some data between them and finally checking if both sides received
+  the proper data.
+
+  A basic PITS test case usually involves opening a listening socket
+  (which in the case of a client-side obfsproxy, emulates the
+  server-side obfspoxy), and a number of outbound connections (which in
+  the case of a client-side obfsproxy, emulate the connections from the
+  Tor client).
+
+  Test case files contain instructions for the sockets of PITS. Through
+  test case files, PITS can be configured to perform the following
+  actions:
+    + Open and close connections
+    + Send arbitrary data through connections
+    + Pause connections
+
+  While conducting the tests, the PITS inbound and outbound sockets
+  record the data they sent and receive in a 'transcript'; after the
+  test is over, the transcript and test case file are post-processed
+  and compared with each other to check whether the intended
+  conversation was performed successfully.
+
+Test case files
+
+  The test case file format is line-oriented; each line is a command,
+  and the first character of the line is a directive followed by a
+  number of arguments.
+  Valid commands are:
+
+     # comment line    - note that # _only_ introduces a comment at the beginning
+                         of a line; elsewhere, it's either a syntax error or part
+                         of an argument
+
+     P number          - pause test-case execution for |number| milliseconds
+     ! <n>             - initiate connection with identifier <n>
+     * <n>             - Close connection <n> (through inbound socket)
+     > <n> <text>      - transmit <text> on <n> through outbound socket
+     < <n> <text>      - transmit <text> on <n> through inbound socket
+
+  Trailing whitespace is ignored.
+
+  Test cases have to close all established connections explicitly,
+  otherwise the test won't be validated correctly.
+
+Transcript files
+
+  Inbound and outbound sockets log received data to a transcript
+  file. The transcript file format is similar to the test case format:
+
+     ! <n>          - connection <n> established on inbound socket
+     > <text>       - <text> received on inbound socket
+     < <text>       - <text> received on outbound socket.
+     * <n>          - connection <n> destroyed on inbound socket
+
+  
+
+Test case results
+
+  After a test case is completed and the transcript file is written,
+  PITS needs to evalute whether the test case was successful; that is,
+  whether the transcript file correctly describes the test case.
+
+  Because of the properties of TCP, the following post-processing
+  happens to validate the transcript file with the test case file:
+
+  a) Both files are segregated: all the traffic and events of inbound
+     sockets are put on top, and the traffic and events of outbound
+     sockets are put on the bottom.
+
+     (This happens because TCP can't guarantee order of event arival in
+     one direction relative to the order of event arrival in the other
+     direction.)
+
+  b) In both files, for each socket identifier, we concatenate all its
+     traffic in a single 'transmit' directive. In the end, we place the
+     transmit line below the events (session establishment, etc.).
+
+     (This happens because TCP is a stream protocol.)
+
+  c) We string compare the transcript and test-case files.
+
+  XXX document any unexpected behaviors or untestable cases caused by
+  the above postprocessing.
+
+Acknowledgements
+
+  The script file format and the basic idea of PITS are concepts of
+  Zack Weinberg. They were implemented as part of Stegotorus:
+  https://gitweb.torproject.org/stegotorus.git/blob/HEAD:/src/test/tltester.cc
diff --git a/obfsproxy/test/int_tests/pits_network.py b/obfsproxy/test/int_tests/pits_network.py
new file mode 100644
index 0000000..0d093c9
--- /dev/null
+++ b/obfsproxy/test/int_tests/pits_network.py
@@ -0,0 +1,168 @@
+from twisted.internet.protocol import Protocol, Factory, ClientFactory
+from twisted.internet import reactor, error, address, tcp
+
+import logging
+
+class GenericProtocol(Protocol):
+    """
+    Generic PITS connection. Contains useful methods and attributes.
+    """
+    def __init__(self, identifier, direction, transcript, conn_handler):
+        self.identifier = identifier
+        self.direction = direction
+        self.transcript = transcript
+        self.conn_handler = conn_handler
+
+        self.closed = False
+
+        self.conn_handler.register_conn(self, self.identifier, self.direction)
+
+        # If it's inbound, note the connection establishment to the transcript.
+        if self.direction == 'inbound':
+            self.transcript.write('! %s' % self.identifier)
+        logging.debug("Registered '%s' connection with identifier %s" % (direction, identifier))
+
+    def connectionLost(self, reason):
+        logging.debug("%s: Connection was lost (%s)." % (self.name, reason.getErrorMessage()))
+
+        # If it's inbound, note the connection fail to the transcript.
+        if self.direction == 'inbound':
+            self.transcript.write('* %s' % self.identifier)
+
+        self.close()
+
+    def connectionFailed(self, reason):
+        logging.warning("%s: Connection failed to connect (%s)." % (self.name, reason.getErrorMessage()))
+        # XXX Note connection fail to transcript?
+        self.close()
+
+    def dataReceived(self, data):
+        logging.debug("'%s' connection '%s' received %s" % (self.direction, self.identifier, repr(data)))
+
+        # Note data to the transcript.
+        symbol = '>' if self.direction == 'inbound' else '<'
+        self.transcript.write('%s %s %s' % (symbol, self.identifier, data.encode("string_escape")))
+
+    def write(self, buf):
+        """
+        Write 'buf' to the underlying transport.
+        """
+        logging.debug("Connection '%s' writing %s" % (self.identifier, repr(buf)))
+        self.transport.write(buf)
+
+    def close(self):
+        """
+        Close the connection.
+        """
+        if self.closed: return # NOP if already closed
+
+        logging.debug("%s: Closing connection." % self.name)
+
+        self.transport.loseConnection()
+
+        self.conn_handler.unregister_conn(self.identifier, self.direction)
+
+        self.closed = True
+
+class OutboundConnection(GenericProtocol):
+    def __init__(self, identifier, transcript, conn_handler):
+        self.name = "out_%s_%s" % (identifier, hex(id(self)))
+        GenericProtocol.__init__(self, identifier, 'outbound', transcript, conn_handler)
+
+class InboundConnection(GenericProtocol):
+    def __init__(self, identifier, transcript, conn_handler):
+        self.name = "in_%s_%s" % (identifier, hex(id(self)))
+        GenericProtocol.__init__(self, identifier, 'inbound', transcript, conn_handler)
+
+class PITSOutboundFactory(Factory):
+    """
+    Outbound PITS factory.
+    """
+    def __init__(self, identifier, transcript, conn_handler):
+        self.transcript = transcript
+        self.conn_handler = conn_handler
+
+        self.identifier = identifier
+        self.name = "out_factory_%s" % hex(id(self))
+
+    def buildProtocol(self, addr):
+        # New outbound connection.
+        return OutboundConnection(self.identifier, self.transcript, self.conn_handler)
+
+    def startFactory(self):
+        logging.debug("%s: Started up PITS outbound listener." % self.name)
+
+    def stopFactory(self):
+        logging.debug("%s: Shutting down PITS outbound listener." % self.name)
+
+    def startedConnecting(self, connector):
+        logging.debug("%s: Client factory started connecting." % self.name)
+
+    def clientConnectionLost(self, connector, reason):
+        logging.debug("%s: Connection lost (%s)." % (self.name, reason.getErrorMessage()))
+
+    def clientConnectionFailed(self, connector, reason):
+        logging.debug("%s: Connection failed (%s)." % (self.name, reason.getErrorMessage()))
+
+
+class PITSInboundFactory(Factory):
+    """
+    Inbound PITS factory
+    """
+    def __init__(self, transcript, conn_handler):
+        self.transcript = transcript
+        self.conn_handler = conn_handler
+
+        self.name = "in_factory_%s" % hex(id(self))
+
+        # List with all the identifiers observed while parsing the
+        # test case file so far.
+        self.identifiers_seen = []
+        # The number of identifiers used so far to name incoming
+        # connections. Normally it should be smaller than the length
+        # of 'identifiers_seen'.
+        self.identifiers_used_n = 0
+
+    def buildProtocol(self, addr):
+        # New inbound connection.
+        identifier = self._get_identifier_for_new_conn()
+        return InboundConnection(identifier, self.transcript, self.conn_handler)
+
+    def register_new_identifier(self, identifier):
+        """Register new connection identifier."""
+
+        if identifier in self.identifiers_seen:
+            # The identifier was already in our list. Broken test case
+            # or broken PITS.
+            logging.warning("Tried to register identifier '%s' more than once (list: %s)."
+                            "Maybe your test case is broken, or this could be a bug." %
+                            (identifier, self.identifiers_seen))
+            return
+
+        self.identifiers_seen.append(identifier)
+
+    def _get_identifier_for_new_conn(self):
+        """
+        We got a new incoming connection. Find the next identifier
+        that we should use, and return it.
+        """
+        # BUG len(identifiers_seen) == 0 , identifiers_used == 0
+        # NORMAL len(identifiers_seen) == 1, identifiers_used == 0
+        # BUG len(identifiers_seen) == 2, identifiers_used == 3
+        if (self.identifiers_used_n >= len(self.identifiers_seen)):
+            logging.warning("Not enough identifiers for new connection (%d, %s)" %
+                            (self.identifiers_used_n, str(self.identifiers_seen)))
+            assert(False)
+
+        identifier = self.identifiers_seen[self.identifiers_used_n]
+        self.identifiers_used_n += 1
+
+        return identifier
+
+
+    def startFactory(self):
+        logging.debug("%s: Started up PITS inbound listener." % self.name)
+
+    def stopFactory(self):
+        logging.debug("%s: Shutting down PITS inbound listener." % self.name)
+        # XXX here we should close all existiing connections
diff --git a/obfsproxy/test/int_tests/pits_transcript.py b/obfsproxy/test/int_tests/pits_transcript.py
new file mode 100644
index 0000000..db33f70
--- /dev/null
+++ b/obfsproxy/test/int_tests/pits_transcript.py
@@ -0,0 +1,98 @@
+import collections
+import logging
+import pits
+
+class Transcript(object):
+    """
+    Manages the PITS transcript. Also contains the functions that
+    verify the transcript against the test case file.
+
+    Attributes:
+    'text', the transcript text.
+    """
+
+    def __init__(self):
+        self.text = ''
+
+    def write(self, data):
+        """Write 'data' to transcript."""
+
+        self.text += data
+        self.text += '\n'
+
+    def get(self):
+        return self.text
+
+    def test_was_success(self, original_script):
+        """
+        Validate transcript against test case file. Return True if the
+        test was successful and False otherwise.
+        """
+        postprocessed_script = self._postprocess(original_script)
+        postprocessed_transcript = self._postprocess(self.text)
+
+        # Log the results
+        log_func = logging.debug if postprocessed_script == postprocessed_transcript else logging.warning
+        log_func("postprocessed_script:\n'%s'" % postprocessed_script)
+        log_func("postprocessed_transcript:\n'%s'" % postprocessed_transcript)
+
+        return postprocessed_script == postprocessed_transcript
+
+    def _postprocess(self, script):
+        """
+        Post-process a (trans)script, according to the instructions of
+        the "Test case results" section.
+
+        Return the postprocessed string.
+
+        Assume correctly formatted script file.
+        """
+        logging.debug("Postprocessing:\n%s" % script)
+
+        postprocessed = ''
+        outbound_events = [] # Events of the outbound connections
+        inbound_events = [] # Events of the inbound connections
+        # Data towards outbound connections (<identifier> -> <outbound data>)
+        outbound_data = collections.OrderedDict()
+        # Data towards inbound connections (<identifier> -> <inbound data>)
+        inbound_data = collections.OrderedDict()
+
+        for line in script.split('\n'):
+            line = line.rstrip()
+            if line == '':
+                continue
+
+            tokens = line.split(" ")
+
+            if tokens[0] == 'P' or tokens[0] == '#': # Ignore
+                continue
+            elif tokens[0] in ['!', '*']: # Inbound events
+                inbound_events.append(line)
+            elif tokens[0] == '>': # Data towards inbound socket
+                if not tokens[1] in inbound_data:
+                    inbound_data[tokens[1]] = ''
+
+                inbound_data[tokens[1]] += ' '.join(tokens[2:])
+            elif tokens[0] == '<': # Data towards outbound socket
+                if not tokens[1] in outbound_data:
+                    outbound_data[tokens[1]] = ''
+
+                outbound_data[tokens[1]] += ' '.join(tokens[2:])
+
+        """
+        Inbound-related events and traffic go on top, the rest go to
+        the bottom. Event lines go on top, transmit lines on bottom.
+        """
+
+        # Inbound lines
+        postprocessed += '\n'.join(inbound_events)
+        postprocessed += '\n'
+        for identifier, data in inbound_data.items():
+            postprocessed += '> %s %s\n' % (identifier, data)
+
+        # Outbound lines
+        postprocessed += '\n'.join(outbound_events)
+        for identifier, data in outbound_data.items():
+            postprocessed += '< %s %s\n' % (identifier, data)
+
+        return postprocessed
diff --git a/obfsproxy/test/int_tests/test_case.pits b/obfsproxy/test/int_tests/test_case.pits
new file mode 100644
index 0000000..aebd297
--- /dev/null
+++ b/obfsproxy/test/int_tests/test_case.pits
@@ -0,0 +1,16 @@
+# Sample test case
+
+! one
+> one ABC
+< one DEF
+< one HIJ
+> one KLM
+P 1
+! two
+! three
+* one
+> two 123
+> two 456
+* three
+< two 789
+* two
diff --git a/obfsproxy/test/int_tests/test_case_simple.pits b/obfsproxy/test/int_tests/test_case_simple.pits
new file mode 100644
index 0000000..9938b69
--- /dev/null
+++ b/obfsproxy/test/int_tests/test_case_simple.pits
@@ -0,0 +1,6 @@
+! one
+> one ABC
+! two
+< two DFG
+* one
+* two
\ No newline at end of file
diff --git a/obfsproxy/test/int_tests/test_pits.py b/obfsproxy/test/int_tests/test_pits.py
new file mode 100644
index 0000000..3bcc8bb
--- /dev/null
+++ b/obfsproxy/test/int_tests/test_pits.py
@@ -0,0 +1,43 @@
+import os
+import logging
+
+import pits
+
+import twisted.trial.unittest as unittest
+
+class PITSTest(unittest.TestCase):
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        self.treader.cleanup()
+
+    def _doTest(self, transport_name, test_case_file):
+        self.treader = pits.TestReader(self.assertTrue, test_case_file)
+        return self.treader.do_test(
+            ('%s' % transport_name,
+             'client',
+             '127.0.0.1:%d' % pits.CLIENT_OBFSPORT,
+             '--dest=127.0.0.1:%d' % pits.SERVER_OBFSPORT),
+            ('%s' % transport_name,
+             'server',
+             '127.0.0.1:%d' % pits.SERVER_OBFSPORT,
+             '--dest=127.0.0.1:%d' % self.treader.pits.get_pits_inbound_address().port))
+
+    # XXX This is pretty ridiculous. Find a smarter way to make up for the
+    # absense of load_tests().
+    def test_dummy_1(self):
+        return self._doTest("dummy", "../test_case.pits")
+
+    def test_dummy_2(self):
+        return self._doTest("dummy", "../test_case_simple.pits")
+
+    def test_obfs2_1(self):
+        return self._doTest("obfs2", "../test_case.pits")
+
+    def test_obfs2_2(self):
+        return self._doTest("obfs2", "../test_case_simple.pits")
+
+if __name__ == '__main__':
+    from unittest import main
+    main()

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-privacy/packages/obfsproxy.git



More information about the Pkg-privacy-commits mailing list