[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