commit a99ca9dd19d3fc8e290b04ad3af9251a43caa4d1 Author: Zack Weinberg zackw@panix.com Date: Tue Jul 26 15:44:08 2011 -0700
Flesh tester.py.in out to subsume the existing non-automated integration tests. Note: all tests presently fail due to bugs introduced somewhere on this branch. --- src/test/integration_test/alpha | 38 ---- src/test/integration_test/int_test.sh | 76 ------- src/test/test_socks_unsupported.py | 18 -- src/test/tester.py.in | 354 ++++++++++++++++++++++++++++++++- 4 files changed, 348 insertions(+), 138 deletions(-)
diff --git a/src/test/integration_test/alpha b/src/test/integration_test/alpha deleted file mode 100644 index b6d68d6..0000000 --- a/src/test/integration_test/alpha +++ /dev/null @@ -1,38 +0,0 @@ -THIS IS A TEST FILE. IT'S USED BY THE INTEGRATION TESTS. -THIS IS A TEST FILE. IT'S USED BY THE INTEGRATION TESTS. -THIS IS A TEST FILE. IT'S USED BY THE INTEGRATION TESTS. -THIS IS A TEST FILE. IT'S USED BY THE INTEGRATION TESTS. - -"Can entropy ever be reversed?" -"THERE IS AS YET INSUFFICIENT DATA FOR A MEANINGFUL ANSWER." -"Can entropy ever be reversed?" -"THERE IS AS YET INSUFFICIENT DATA FOR A MEANINGFUL ANSWER." -"Can entropy ever be reversed?" -"THERE IS AS YET INSUFFICIENT DATA FOR A MEANINGFUL ANSWER." -"Can entropy ever be reversed?" -"THERE IS AS YET INSUFFICIENT DATA FOR A MEANINGFUL ANSWER." -"Can entropy ever be reversed?" -"THERE IS AS YET INSUFFICIENT DATA FOR A MEANINGFUL ANSWER." -"Can entropy ever be reversed?" -"THERE IS AS YET INSUFFICIENT DATA FOR A MEANINGFUL ANSWER." -"Can entropy ever be reversed?" -"THERE IS AS YET INSUFFICIENT DATA FOR A MEANINGFUL ANSWER." -"Can entropy ever be reversed?" -"THERE IS AS YET INSUFFICIENT DATA FOR A MEANINGFUL ANSWER." - - In obfuscatory age geeky warfare did I wage - For hiding bits from nasty censors' sight - I was hacker to my set in that dim dark age of net - And I hacked from noon till three or four at night - - Then a rival from Helsinki said my protocol was dinky - So I flamed him with a condescending laugh, - Saying his designs for stego might as well be made of lego - And that my bikeshed was prettier by half. - - But Claude Shannon saw my shame. From his noiseless channel came - A message sent with not a wasted byte - "There are nine and sixty ways to disguise communiques - And RATHER MORE THAN ONE OF THEM IS RIGHT" - - (apologies to Rudyard Kipling.) diff --git a/src/test/integration_test/int_test.sh b/src/test/integration_test/int_test.sh deleted file mode 100644 index 9aa37ac..0000000 --- a/src/test/integration_test/int_test.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/bin/bash - -# replace this with your path to obfsproxy. -OBFSPROXY="../../../obfsproxy" -# replace this with your path to ncat. -NCAT=ncat - -ENTRY_PORT=4999 -SERVER_PORT=5000 -NCAT_PORT=5001 - -DIR=inttemp_temp -FILE1=$DIR/test1 -FILE2=$DIR/test2 - -mkdir -p $DIR ; :>$FILE1 - -# TEST 1 -# We open a server and a client and transfer a file. Then we check if the output of the -# server is the same as the file we sent. - -$NCAT -k -l -o $FILE1 -p $NCAT_PORT > /dev/null & -ncat1_pid=$! - - -$OBFSPROXY --log-min-severity=warn obfs2 --dest=127.0.0.1:$NCAT_PORT server 127.0.0.1:$SERVER_PORT \ - + obfs2 --dest=127.0.0.1:$SERVER_PORT client 127.0.0.1:$ENTRY_PORT & -obfsproxy_pid=$! -sleep 1 - - -$NCAT localhost $ENTRY_PORT < alpha & -ncat2_pid=$! -sleep 2 - -if cmp -s alpha $FILE1 -then echo "GREAT SUCCESS 1!" ; rm $FILE1 -else echo "GREAT FAIL 1!" -fi - -kill -9 $ncat1_pid -kill -9 $obfsproxy_pid -kill -9 $ncat2_pid - -sleep 2 - -# TEST 2 -# We open an obfsproxy SOCKS server on the dummy protocol and an ncat listening. -# Then we configure another ncat to use SOCKS4 and transfer a file to the other ncat. -# Finally, we check if the file was sent correctly. - -:>$FILE2 - -$NCAT -k -l -o $FILE2 -p $NCAT_PORT > /dev/null & -ncat1_pid=$! - -$OBFSPROXY --log-min-severity=warn dummy socks 127.0.0.1:$SERVER_PORT & -obfsproxy_pid=$! -sleep 1 - -$NCAT --proxy-type socks4 --proxy 127.0.0.1:$SERVER_PORT \ - 127.0.0.1 $NCAT_PORT < alpha & -ncat2_pid=$! -sleep 2 - -if cmp -s alpha $FILE2 -then echo "GREAT SUCCESS 2!" ; rm $FILE2 -else echo "GREAT FAIL 2!" -fi - -kill -9 $ncat1_pid -kill -9 $obfsproxy_pid -kill -9 $ncat2_pid - -rmdir $DIR - diff --git a/src/test/test_socks_unsupported.py b/src/test/test_socks_unsupported.py deleted file mode 100644 index d1afd02..0000000 --- a/src/test/test_socks_unsupported.py +++ /dev/null @@ -1,18 +0,0 @@ -import socket,struct - -negot = struct.pack('BBB', 5, 1, 0) -request = struct.pack('BBBBBBB', 5, 2, 0, 1, 1, 1, 1) - -PORT = 4500 - -s=socket.socket(socket.AF_INET, socket.SOCK_STREAM) -s.connect(("127.0.0.1",PORT)) -s.send(negot) -s.recv(1024) -s.send(request) -data = s.recv(1024) -if (struct.unpack('BBBBih', data)[1] == 7): - print "Works." -else: - print "Fail!" - diff --git a/src/test/tester.py.in b/src/test/tester.py.in index d330e69..b0602ed 100644 --- a/src/test/tester.py.in +++ b/src/test/tester.py.in @@ -5,14 +5,356 @@ # The obfsproxy binary is assumed to exist in the current working # directory, and you need to have Python 2.6 or better (but not 3). # You need to be able to make connections to arbitrary high-numbered -# TCP ports on the loopback interface (there is, however, an effort to -# figure out which ones are already in use) and all IPv4 addresses in -# 127.0.0/24 are assumed to be bound to the loopback interface. +# TCP ports on the loopback interface.
+import errno +import multiprocessing +import Queue +import signal import socket import struct import subprocess -import sys +import time +import unittest
-print "All tests successful." -sys.exit(0) +# Helper: Repeatedly try to connect to the specified server socket +# until either it succeeds or one full second has elapsed. (Surely +# there is a better way to do this?) + +def connect_with_retry(addr): + retry = 0 + while True: + try: + return socket.create_connection(addr) + except socket.error, e: + if e.errno != errno.ECONNREFUSED: raise + if retry == 20: raise + retry += 1 + time.sleep(0.05) + +# Helper: In a separate process (to avoid deadlock), listen on a +# specified socket. The first time something connects to that socket, +# read all available data, stick it in a string, and post the string +# to the output queue. Then close both sockets and exit. + +class ReadWorker(object): + @staticmethod + def work(address, oq): + listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + listener.bind(address) + listener.listen(1) + (conn, remote) = listener.accept() + data = "" + try: + while True: + chunk = conn.recv(4096) + if chunk == "": break + data += chunk + except Exception, e: + data += "|RECV ERROR: " + e + conn.close() + listener.close() + oq.put(data) + + def __init__(self, address): + self.oq = multiprocessing.Queue() + self.worker = multiprocessing.Process(target=self.work, + args=(address, self.oq)) + self.worker.start() + + def get(self): + rv = self.oq.get(timeout=1) + self.worker.join() + return rv + + def stop(self): + if self.worker.is_alive(): self.worker.terminate() + +# Right now this is a direct translation of the former int_test.sh +# (except that I have fleshed out the SOCKS test a bit). +# It will be made more general and parametric Real Soon. + +ENTRY_PORT = 4999 +SERVER_PORT = 5000 +EXIT_PORT = 5001 + +# +# Test base classes. They do _not_ inherit from unittest.TestCase +# so that they are not scanned directly for test functions (some of +# them do provide test functions, but not in a usable state without +# further code from subclasses). +# + +class DirectTest(object): + def setUp(self): + self.output_reader = ReadWorker(("127.0.0.1", EXIT_PORT)) + self.obfs = subprocess.Popen( + self.obfs_args, + stdin=open("/dev/null", "r"), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + self.input_chan = connect_with_retry(("127.0.0.1", ENTRY_PORT)) + self.input_chan.settimeout(1.0) + + def tearDown(self): + if self.obfs.returncode is None: + self.obfs.terminate() + self.output_reader.stop() + + def checkSubprocesses(self): + if self.obfs.poll() is None: + self.obfs.send_signal(signal.SIGINT) + + (out, err) = self.obfs.communicate() + + if (out != "" or err != "" or self.obfs.returncode != 0): + self.fail("obfsproxy process failure:\n" + "\treturn code: %d\n" + "\tstdout: %s\n" + "\tstderr: %s\n" + % (self.obfs.returncode, out, err)) + + def test_direct_transfer(self): + # Open a server and a simple client (in the same process) and + # transfer a file. Then check whether the output is the same + # as the input. + self.input_chan.sendall(TEST_FILE) + self.input_chan.shutdown(socket.SHUT_WR) + try: + output = self.output_reader.get() + except Queue.Empty: + output = "" + + self.checkSubprocesses() + self.assertEqual(output, TEST_FILE) + +# Same as above, but we use a socks client instead of a simple client, +# and the server's a separate process. + +class SocksTest(object): + def setUp(self): + self.output_reader = ReadWorker(("127.0.0.1", EXIT_PORT)) + self.obfs_server = subprocess.Popen( + self.server_args, + stdin=open("/dev/null", "r"), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + self.obfs_client = subprocess.Popen( + self.client_args, + stdin=open("/dev/null", "r"), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + self.input_chan = connect_with_retry(("127.0.0.1", ENTRY_PORT)) + self.input_chan.settimeout(1.0) + + def tearDown(self): + if self.obfs_server.returncode is None: + self.obfs_server.terminate() + if self.obfs_client.returncode is None: + self.obfs_client.terminate() + self.output_reader.stop() + + def checkSubprocesses(self): + if self.obfs_server.poll() is None: + self.obfs_server.send_signal(signal.SIGINT) + if self.obfs_client.poll() is None: + self.obfs_client.send_signal(signal.SIGINT) + + (sout, serr) = self.obfs_server.communicate() + (cout, cerr) = self.obfs_client.communicate() + + if (sout != "" or serr != "" or cout != "" or cerr != "" + or self.obfs_server.returncode != 0 + or self.obfs_client.returncode != 0): + self.fail("obfsproxy process failures:\n" + "\tclient return code: %d\n" + "\tserver return code: %d\n" + "\tclient stdout: %s\n" + "\tclient stderr: %s\n" + "\tserver stdout: %s\n" + "\tserver stderr: %s\n" + % (self.obfs_client.returncode, + self.obfs_server.returncode, + cout, cerr, sout, serr)) + + # 'sequence' is a sequence of SOCKS[45] protocol messages + # which we will send or receive. Sends alternate with + # receives. Each entry may be a string, which is sent or + # received verbatim; a pair of a sequence of data items and a + # struct pack code, which is packed and then sent or received; + # or the constant False, which means the server is expected to + # drop the connection at that point. If we come to the end of + # the SOCKS sequence without the server having dropped the + # connection, we transmit the test file and expect to get it + # back from the far end. + def socksTest(self, sequence): + sending = True + good = True + for msg in sequence: + if msg is False: + self.input_chan.shutdown(socket.SHUT_WR) + # Expect either a clean closedown or a connection reset + # at this point. + got = "" + try: + got = self.input_chan.recv(4096) + except socket.error, e: + if e.errno != errno.ECONNRESET: raise + self.assertEqual(got, "") + good = False + break + elif isinstance(msg, str): + exp = msg + elif isinstance(msg, tuple): + exp = struct.pack(msg[1], *msg[0]) + else: + raise TypeError("incomprehensible msg: " + repr(msg)) + if sending: + self.input_chan.sendall(exp) + else: + got = "" + try: + got = self.input_chan.recv(4096) + except socket.error, e: + if e.errno != errno.ECONNRESET: raise + self.assertEqual(got, exp) + if good: + self.input_chan.sendall(TEST_FILE) + self.input_chan.shutdown(socket.SHUT_WR) + try: + output = self.output_reader.get() + except Queue.Empty: + output = "" + self.assertEqual(output, TEST_FILE) + + self.input_chan.close() + self.checkSubprocesses() + + def test_illformed(self): + # ill-formed socks message - server should drop connection + self.socksTest([ "GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n" + "Connection: close\r\n\r\n", + False ]) + + def test_socks4_unsupported_methods(self): + # SOCKS4 bind request - should fail, presently just drops connection + self.socksTest([ ( (4, 2, SERVER_PORT, 127, 0, 0, 1, 0), "!BBH5B" ), + False ]) + + def test_socks4_connect(self): + # SOCKS4 connection request - should succeed + self.socksTest([ ( (4, 1, SERVER_PORT, 127, 0, 0, 1, 0), "!BBH5B" ), + ( (0, 90, SERVER_PORT, 127, 0, 0, 1), "!BBH4B" ) ]) + + def test_socks5_bad_handshakes(self): + self.socksTest([ "\x05", False ]) + self.socksTest([ "\x05\x00", False ]) + self.socksTest([ "\x05\x01\x01", "\x05\xFF", False ]) + self.socksTest([ "\x05\x01\x080", "\x05\xFF", False ]) + self.socksTest([ "\x05\x02\x01\x02", "\x05\xFF", False ]) + + def test_socks5_bad_requests(self): + self.socksTest([ "\x05\x01\x00", "\x05\x00", "\x05\x00", + "\x05\x07\x00", False ]) + self.socksTest([ "\x05\x02\x00\x01", "\x05\x00", "\x05\x00", + "\x05\x07\x00", False ]) + + def test_socks5_unsupported_methods(self): + self.socksTest([ "\x05\x01\x00", "\x05\x00", + ( (5, 2, 0, 1, 127, 0, 0, 1, SERVER_PORT), "!8BH" ), + "\x05\x07\x00", False ]) + self.socksTest([ "\x05\x01\x00", "\x05\x00", + ( (5, 3, 0, 1, 127, 0, 0, 1, SERVER_PORT), "!8BH" ), + "\x05\x07\x00", False ]) + + def test_socks5_connect(self): + self.socksTest([ "\x05\x01\x00", "\x05\x00", + ( (5, 1, 0, 1, 127, 0, 0, 1, SERVER_PORT), "!8BH" ), + ( (5, 0, 0, 1, 127, 0, 0, 1, SERVER_PORT), "!8BH" ) ]) + +# +# Concrete test classes specialize the above base classes for each protocol. +# + +class DirectObfs2(DirectTest, unittest.TestCase): + obfs_args = ("./obfsproxy", "--log-min-severity=warn", + "obfs2", + "--dest=127.0.0.1:%d" % EXIT_PORT, + "server", "127.0.0.1:%d" % SERVER_PORT, + "+", "obfs2", + "--dest=127.0.0.1:%d" % SERVER_PORT, + "client", "127.0.0.1:%d" % ENTRY_PORT) + +class DirectDummy(DirectTest, unittest.TestCase): + obfs_args = ("./obfsproxy", "--log-min-severity=warn", + "dummy", "server", + "127.0.0.1:%d" % SERVER_PORT, + "127.0.0.1:%d" % EXIT_PORT, + "+", "dummy", "client", + "127.0.0.1:%d" % ENTRY_PORT, + "127.0.0.1:%d" % SERVER_PORT) + +class SocksObfs2(SocksTest, unittest.TestCase): + server_args = ("./obfsproxy", "--log-min-severity=warn", + "obfs2", + "--dest=127.0.0.1:%d" % EXIT_PORT, + "server", "127.0.0.1:%d" % SERVER_PORT) + client_args = ("./obfsproxy", "--log-min-severity=warn", + "obfs2", + "socks", "127.0.0.1:%d" % ENTRY_PORT) + +class SocksDummy(SocksTest, unittest.TestCase): + server_args = ("./obfsproxy", "--log-min-severity=warn", + "dummy", "server", + "127.0.0.1:%d" % SERVER_PORT, + "127.0.0.1:%d" % EXIT_PORT) + client_args = ("./obfsproxy", "--log-min-severity=warn", + "dummy", "socks", + "127.0.0.1:%d" % ENTRY_PORT) + +TEST_FILE = """\ +THIS IS A TEST FILE. IT'S USED BY THE INTEGRATION TESTS. +THIS IS A TEST FILE. IT'S USED BY THE INTEGRATION TESTS. +THIS IS A TEST FILE. IT'S USED BY THE INTEGRATION TESTS. +THIS IS A TEST FILE. IT'S USED BY THE INTEGRATION TESTS. + +"Can entropy ever be reversed?" +"THERE IS AS YET INSUFFICIENT DATA FOR A MEANINGFUL ANSWER." +"Can entropy ever be reversed?" +"THERE IS AS YET INSUFFICIENT DATA FOR A MEANINGFUL ANSWER." +"Can entropy ever be reversed?" +"THERE IS AS YET INSUFFICIENT DATA FOR A MEANINGFUL ANSWER." +"Can entropy ever be reversed?" +"THERE IS AS YET INSUFFICIENT DATA FOR A MEANINGFUL ANSWER." +"Can entropy ever be reversed?" +"THERE IS AS YET INSUFFICIENT DATA FOR A MEANINGFUL ANSWER." +"Can entropy ever be reversed?" +"THERE IS AS YET INSUFFICIENT DATA FOR A MEANINGFUL ANSWER." +"Can entropy ever be reversed?" +"THERE IS AS YET INSUFFICIENT DATA FOR A MEANINGFUL ANSWER." +"Can entropy ever be reversed?" +"THERE IS AS YET INSUFFICIENT DATA FOR A MEANINGFUL ANSWER." + + In obfuscatory age geeky warfare did I wage + For hiding bits from nasty censors' sight + I was hacker to my set in that dim dark age of net + And I hacked from noon till three or four at night + + Then a rival from Helsinki said my protocol was dinky + So I flamed him with a condescending laugh, + Saying his designs for stego might as well be made of lego + And that my bikeshed was prettier by half. + + But Claude Shannon saw my shame. From his noiseless channel came + A message sent with not a wasted byte + "There are nine and sixty ways to disguise communiques + And RATHER MORE THAN ONE OF THEM IS RIGHT" + + (apologies to Rudyard Kipling.) +""" + +if __name__ == '__main__': + unittest.main()
tor-commits@lists.torproject.org