commit f6ca5f1d2cfd7b32a957cba14624773089a3e27f Author: Ximin Luo infinity0@gmx.com Date: Tue Oct 15 23:18:11 2013 +0100
split Makefile into source-level vs binary-level packaging scripts - Makefile retains the same behaviour as before and builds dist/dist-exe - Makefile.client acts on the client component only, following GNU standards --- Makefile | 133 ++++++++------- Makefile.client | 98 +++++++++++ flashproxy-client-test | 402 --------------------------------------------- flashproxy-client-test.py | 401 ++++++++++++++++++++++++++++++++++++++++++++ setup-client-exe.py | 1 + setup-common.py | 20 +++ 6 files changed, 595 insertions(+), 460 deletions(-)
diff --git a/Makefile b/Makefile index d8c85cb..24ef960 100644 --- a/Makefile +++ b/Makefile @@ -1,71 +1,88 @@ +# Makefile for a self-contained binary distribution of flashproxy-client. +# +# This builds two zipball targets, dist and dist-exe, for POSIX and Windows +# respectively. Both can be extracted and run in-place by the end user. +# (PGP-signed forms also exist, sign and sign-exe.) +# +# If you are a distro packager, instead see the separate build scripts for each +# source component, all of which have an `install` target: +# - client: Makefile.client +# - common: setup-common.py +# - facilitator: facilitator/{configure.ac,Makefile.am} +# +# Not for the faint-hearted: it is possible to build dist-exe on GNU/Linux by +# using wine to install the windows versions of Python, py2exe, and m2crypto, +# then running `make PYTHON_W32="wine python" dist-exe`. + +PACKAGE = flashproxy-client VERSION = 1.3 +DISTNAME = $(PACKAGE)-$(VERSION)
-DESTDIR = -PREFIX = /usr/local -BINDIR = $(PREFIX)/bin -MANDIR = $(PREFIX)/share/man +THISFILE = $(lastword $(MAKEFILE_LIST)) +PYTHON = python +PYTHON_W32 = $(PYTHON)
-PYTHON ?= python -export PY2EXE_TMPDIR = py2exe-tmp +MAKE_CLIENT = $(MAKE) -f Makefile.client PYTHON="$(PYTHON)"
-CLIENT_BIN = flashproxy-client flashproxy-reg-appspot flashproxy-reg-email flashproxy-reg-http flashproxy-reg-url -CLIENT_MAN = doc/flashproxy-client.1 doc/flashproxy-reg-appspot.1 doc/flashproxy-reg-email.1 doc/flashproxy-reg-http.1 doc/flashproxy-reg-url.1 -CLIENT_DIST_FILES = $(CLIENT_BIN) README LICENSE ChangeLog torrc -CLIENT_DIST_DOC_FILES = $(CLIENT_MAN) -CLIENT_DIST_LIB_COMMON = flashproxy/__init__.py flashproxy/keys.py flashproxy/util.py +# all is N/A for a binary package, but include for completeness +all: dist
-all: $(CLIENT_DIST_FILES) $(CLIENT_MAN) - : +DISTDIR = dist/$(DISTNAME) +$(DISTDIR): Makefile.client setup-common.py $(THISFILE) + mkdir -p $(DISTDIR) + $(MAKE_CLIENT) DESTDIR=$(DISTDIR) bindir=/ docdir=/ man1dir=/doc/ \ + install + $(PYTHON) setup-common.py build_py -d $(DISTDIR)
-%.1: %.1.txt - rm -f $@ - a2x --no-xmllint --xsltproc-opts "--stringparam man.th.title.max.length 24" -d manpage -f manpage $< +dist/%.zip: dist/% + cd dist && zip -q -r -9 "$(@:dist/%=%)" "$(<:dist/%=%)"
-install: - mkdir -p $(DESTDIR)$(BINDIR) - mkdir -p $(DESTDIR)$(MANDIR)/man1 - cp -f $(CLIENT_BIN) $(DESTDIR)$(BINDIR) - cp -f $(CLIENT_MAN) $(DESTDIR)$(MANDIR)/man1 +dist/%.zip.asc: dist/%.zip + rm -f "$@" + gpg --sign --detach-sign --armor "$<" + gpg --verify "$@" "$<"
-DISTNAME = flashproxy-client-$(VERSION) -DISTDIR = dist/$(DISTNAME) -dist: $(CLIENT_MAN) - rm -rf dist - mkdir -p $(DISTDIR) - mkdir $(DISTDIR)/doc - cp -f $(CLIENT_DIST_FILES) $(DISTDIR) - cp -f $(CLIENT_DIST_DOC_FILES) $(DISTDIR)/doc - test -n "$(CLIENT_DIST_LIB_COMMON)" && \ - { mkdir $(DISTDIR)/flashproxy && \ - cp -f $(CLIENT_DIST_LIB_COMMON) $(DISTDIR)/flashproxy; } || true - cd dist && zip -q -r -9 $(DISTNAME).zip $(DISTNAME) - -dist/$(DISTNAME).zip: $(CLIENT_DIST_FILES) - $(MAKE) dist - -sign: dist/$(DISTNAME).zip - rm -f dist/$(DISTNAME).zip.asc - cd dist && gpg --sign --detach-sign --armor $(DISTNAME).zip - cd dist && gpg --verify $(DISTNAME).zip.asc $(DISTNAME).zip - -$(PY2EXE_TMPDIR)/dist: $(CLIENT_BIN) - rm -rf $(PY2EXE_TMPDIR) - $(PYTHON) setup-client-exe.py py2exe -q - -dist-exe: DISTNAME := $(DISTNAME)-win32 -dist-exe: CLIENT_BIN := $(PY2EXE_TMPDIR)/dist/* -dist-exe: CLIENT_MAN := $(addsuffix .txt,$(CLIENT_MAN)) -dist-exe: CLIENT_DIST_LIB_COMMON :=# py2exe static-links dependencies -# Delegate to the "dist" target using the substitutions above. -dist-exe: $(PY2EXE_TMPDIR)/dist setup-client-exe.py dist - -clean: - rm -f *.pyc +dist: force-dist $(DISTDIR).zip + +sign: force-dist $(DISTDIR).zip.asc + +PY2EXE_TMPDIR = py2exe-tmp +export PY2EXE_TMPDIR +$(PY2EXE_TMPDIR): setup-client-exe.py + $(PYTHON_W32) setup-client-exe.py py2exe -q + +DISTDIR_W32 = $(DISTDIR)-win32 +# below, we override DST_SCRIPT and DST_MAN1 for windows +$(DISTDIR_W32): $(PY2EXE_TMPDIR) $(THISFILE) + mkdir -p $(DISTDIR_W32) + $(MAKE_CLIENT) DESTDIR=$(DISTDIR_W32) bindir=/ docdir=/ man1dir=/doc/ \ + DST_SCRIPT= DST_MAN1='$$(SRC_MAN1)' \ + install + cp -t $(DISTDIR_W32) $(PY2EXE_TMPDIR)/dist/* + +dist-exe: force-dist-exe $(DISTDIR_W32).zip + +sign-exe: force-dist-exe $(DISTDIR_W32).zip.asc + +# clean is N/A for a binary package, but include for completeness +clean: distclean + +distclean: + $(MAKE_CLIENT) clean + $(PYTHON) setup-common.py clean --all rm -rf dist $(PY2EXE_TMPDIR)
-test: - ./flashproxy-client-test +test: check +check: + $(MAKE_CLIENT) check + $(PYTHON) setup-common.py check cd facilitator && ./facilitator-test cd proxy && ./flashproxy-test.js
-.PHONY: all install dist sign dist-exe clean test +force-dist: + rm -rf $(DISTDIR) $(DISTDIR).zip + +force-dist-exe: + rm -rf $(DISTDIR_W32) $(DISTDIR_W32).zip $(PY2EXE_TMPDIR) + +.PHONY: all dist sign dist-exe sign-exe clean distclean test check force-dist force-dist-exe diff --git a/Makefile.client b/Makefile.client new file mode 100644 index 0000000..5aec18a --- /dev/null +++ b/Makefile.client @@ -0,0 +1,98 @@ +# Makefile for a source distribution of flashproxy-client. +# +# This package is not self-contained and the build products may require other +# dependencies to function; it is given as a reference for distro packagers. + +PACKAGE = flashproxy-client +VERSION = 1.3 +DISTNAME = $(PACKAGE)-$(VERSION) +DESTDIR = + +THISFILE = $(lastword $(MAKEFILE_LIST)) +PYTHON = python + +# GNU command variables +# see http://www.gnu.org/prep/standards/html_node/Command-Variables.html + +INSTALL = install +INSTALL_DATA = $(INSTALL) -m 644 +INSTALL_PROGRAM = $(INSTALL) +INSTALL_SCRIPT = $(INSTALL) + +# GNU directory variables +# see http://www.gnu.org/prep/standards/html_node/Directory-Variables.html + +prefix = /usr/local +exec_prefix = $(prefix) +bindir = $(exec_prefix)/bin + +datarootdir = $(prefix)/share +datadir = $(datarootdir) +sysconfdir = $(prefix)/etc + +docdir = $(datarootdir)/doc/$(PACKAGE) +mandir = $(datarootdir)/man +man1dir = $(mandir)/man1 + +srcdir = . + +SRC_MAN1 = doc/flashproxy-client.1.txt doc/flashproxy-reg-appspot.1.txt doc/flashproxy-reg-email.1.txt doc/flashproxy-reg-http.1.txt doc/flashproxy-reg-url.1.txt +SRC_SCRIPT = flashproxy-client flashproxy-reg-appspot flashproxy-reg-email flashproxy-reg-http flashproxy-reg-url +SRC_DOC = README LICENSE ChangeLog torrc +SRC_ALL = $(SRC_SCRIPT) $(SRC_DOC) $(SRC_MAN1) + +DST_MAN1 = $(SRC_MAN1:%.1.txt=%.1) +DST_SCRIPT = $(SRC_SCRIPT) +DST_DOC = $(SRC_DOC) +DST_ALL = $(DST_SCRIPT) $(DST_DOC) $(DST_MAN1) + +TEST_PY = flashproxy-client-test.py +TEST_ALL = $(TEST_PY) + +all: $(DST_ALL) $(THISFILE) + +%.1: %.1.txt $(THISFILE) + rm -f $@ + a2x --no-xmllint --xsltproc-opts "--stringparam man.th.title.max.length 24" -d manpage -f manpage $< + +install: all + mkdir -p $(DESTDIR)$(bindir) + for i in $(DST_SCRIPT); do $(INSTALL_SCRIPT) "$$i" $(DESTDIR)$(bindir); done + mkdir -p $(DESTDIR)$(docdir) + for i in $(DST_DOC); do $(INSTALL_DATA) "$$i" $(DESTDIR)$(docdir); done + mkdir -p $(DESTDIR)$(man1dir) + for i in $(DST_MAN1); do $(INSTALL_DATA) "$$i" $(DESTDIR)$(man1dir); done + +uninstall: + for i in $(notdir $(DST_SCRIPT)); do rm $(DESTDIR)$(bindir)/"$$i"; done + for i in $(notdir $(DST_DOC)); do rm $(DESTDIR)$(docdir)/"$$i"; done + for i in $(notdir $(DST_MAN1)); do rm $(DESTDIR)$(man1dir)/"$$i"; done + +clean: + rm -f *.pyc + +distclean: clean + rm -rf $(DISTNAME) + rm -f $(DISTNAME).tar.gz + +maintainer-clean: distclean + rm -f $(DST_MAN1) + +$(DISTNAME): $(SRC_ALL) $(TEST_ALL) $(THISFILE) + mkdir -p $@ + cp --parents -t "$@" $^ + +$(DISTNAME).tar.gz: $(DISTNAME) + tar czf "$@" "$<" + +# we never actually use this target, but it is given for completeness, in case +# distro packagers want to do something with a client-only source tarball +dist: force-dist $(DISTNAME).tar.gz + +check: $(THISFILE) + for i in $(TEST_PY); do $(PYTHON) "$$i"; done + +force-dist: + rm -rf $(DISTNAME) $(DISTNAME).tar.gz + +.PHONY: all install uninstall clean distclean maintainer-clean dist check force-dist diff --git a/flashproxy-client-test b/flashproxy-client-test deleted file mode 100755 index e3c6644..0000000 --- a/flashproxy-client-test +++ /dev/null @@ -1,402 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import base64 -import cStringIO -import httplib -import socket -import subprocess -import sys -import unittest -print sys.path -try: - from hashlib import sha1 -except ImportError: - # Python 2.4 uses this name. - from sha import sha as sha1 - -# Special tricks to load a module whose filename contains a dash and doesn't end -# in ".py". -import imp -dont_write_bytecode = sys.dont_write_bytecode -sys.dont_write_bytecode = True -fp_client = imp.load_source("fp_client", "flashproxy-client") -parse_socks_request = fp_client.parse_socks_request -handle_websocket_request = fp_client.handle_websocket_request -WebSocketDecoder = fp_client.WebSocketDecoder -WebSocketEncoder = fp_client.WebSocketEncoder -sys.dont_write_bytecode = dont_write_bytecode -del dont_write_bytecode -del fp_client - -LOCAL_ADDRESS = ("127.0.0.1", 40000) -REMOTE_ADDRESS = ("127.0.0.1", 40001) - -class TestSocks(unittest.TestCase): - def test_parse_socks_request_empty(self): - self.assertRaises(ValueError, parse_socks_request, "") - def test_parse_socks_request_short(self): - self.assertRaises(ValueError, parse_socks_request, "\x04\x01\x99\x99\x01\x02\x03\x04") - def test_parse_socks_request_ip_userid_missing(self): - dest, port = parse_socks_request("\x04\x01\x99\x99\x01\x02\x03\x04\x00") - dest, port = parse_socks_request("\x04\x01\x99\x99\x01\x02\x03\x04\x00userid") - self.assertEqual((dest, port), ("1.2.3.4", 0x9999)) - def test_parse_socks_request_ip(self): - dest, port = parse_socks_request("\x04\x01\x99\x99\x01\x02\x03\x04userid\x00") - self.assertEqual((dest, port), ("1.2.3.4", 0x9999)) - def test_parse_socks_request_hostname_missing(self): - self.assertRaises(ValueError, parse_socks_request, "\x04\x01\x99\x99\x00\x00\x00\x01userid\x00") - self.assertRaises(ValueError, parse_socks_request, "\x04\x01\x99\x99\x00\x00\x00\x01userid\x00abc") - def test_parse_socks_request_hostname(self): - dest, port = parse_socks_request("\x04\x01\x99\x99\x00\x00\x00\x01userid\x00abc\x00") - -class DummySocket(object): - def __init__(self, read_fd, write_fd): - self.read_fd = read_fd - self.write_fd = write_fd - self.readp = 0 - - def read(self, *args, **kwargs): - self.read_fd.seek(self.readp, 0) - data = self.read_fd.read(*args, **kwargs) - self.readp = self.read_fd.tell() - return data - - def readline(self, *args, **kwargs): - self.read_fd.seek(self.readp, 0) - data = self.read_fd.readline(*args, **kwargs) - self.readp = self.read_fd.tell() - return data - - def recv(self, size, *args, **kwargs): - return self.read(size) - - def write(self, data): - self.write_fd.seek(0, 2) - self.write_fd.write(data) - - def send(self, data, *args, **kwargs): - return self.write(data) - - def sendall(self, data, *args, **kwargs): - return self.write(data) - - def makefile(self, *args, **kwargs): - return self - -def dummy_socketpair(): - f1 = cStringIO.StringIO() - f2 = cStringIO.StringIO() - return (DummySocket(f1, f2), DummySocket(f2, f1)) - -class HTTPRequest(object): - def __init__(self): - self.method = "GET" - self.path = "/" - self.headers = {} - -def transact_http(req): - l, r = dummy_socketpair() - r.send("%s %s HTTP/1.0\r\n" % (req.method, req.path)) - for k, v in req.headers.items(): - r.send("%s: %s\r\n" % (k, v)) - r.send("\r\n") - protocols = handle_websocket_request(l) - - resp = httplib.HTTPResponse(r) - resp.begin() - return resp, protocols - -class TestHandleWebSocketRequest(unittest.TestCase): - DEFAULT_KEY = "0123456789ABCDEF" - DEFAULT_KEY_BASE64 = base64.b64encode(DEFAULT_KEY) - MAGIC_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" - - @staticmethod - def default_req(): - req = HTTPRequest() - req.method = "GET" - req.path = "/" - req.headers["Upgrade"] = "websocket" - req.headers["Connection"] = "Upgrade" - req.headers["Sec-WebSocket-Key"] = TestHandleWebSocketRequest.DEFAULT_KEY_BASE64 - req.headers["Sec-WebSocket-Version"] = "13" - - return req - - def assert_ok(self, req): - resp, protocols = transact_http(req) - self.assertEqual(resp.status, 101) - self.assertEqual(resp.getheader("Upgrade").lower(), "websocket") - self.assertEqual(resp.getheader("Connection").lower(), "upgrade") - self.assertEqual(resp.getheader("Sec-WebSocket-Accept"), base64.b64encode(sha1(self.DEFAULT_KEY_BASE64 + self.MAGIC_GUID).digest())) - self.assertEqual(protocols, []) - - def assert_not_ok(self, req): - resp, protocols = transact_http(req) - self.assertEqual(resp.status // 100, 4) - self.assertEqual(protocols, None) - - def test_default(self): - req = self.default_req() - self.assert_ok(req) - - def test_missing_upgrade(self): - req = self.default_req() - del req.headers["Upgrade"] - self.assert_not_ok(req) - - def test_missing_connection(self): - req = self.default_req() - del req.headers["Connection"] - self.assert_not_ok(req) - - def test_case_insensitivity(self): - """Test that the values of the Upgrade and Connection headers are - case-insensitive.""" - req = self.default_req() - req.headers["Upgrade"] = req.headers["Upgrade"].lower() - self.assert_ok(req) - req.headers["Upgrade"] = req.headers["Upgrade"].upper() - self.assert_ok(req) - req.headers["Connection"] = req.headers["Connection"].lower() - self.assert_ok(req) - req.headers["Connection"] = req.headers["Connection"].upper() - self.assert_ok(req) - - def test_bogus_key(self): - req = self.default_req() - req.headers["Sec-WebSocket-Key"] = base64.b64encode(self.DEFAULT_KEY[:-1]) - self.assert_not_ok(req) - - req.headers["Sec-WebSocket-Key"] = "///" - self.assert_not_ok(req) - - def test_versions(self): - req = self.default_req() - req.headers["Sec-WebSocket-Version"] = "13" - self.assert_ok(req) - req.headers["Sec-WebSocket-Version"] = "8" - self.assert_ok(req) - - req.headers["Sec-WebSocket-Version"] = "7" - self.assert_not_ok(req) - req.headers["Sec-WebSocket-Version"] = "9" - self.assert_not_ok(req) - - del req.headers["Sec-WebSocket-Version"] - self.assert_not_ok(req) - - def test_protocols(self): - req = self.default_req() - req.headers["Sec-WebSocket-Protocol"] = "base64" - resp, protocols = transact_http(req) - self.assertEqual(resp.status, 101) - self.assertEqual(protocols, ["base64"]) - self.assertEqual(resp.getheader("Sec-WebSocket-Protocol"), "base64") - - req = self.default_req() - req.headers["Sec-WebSocket-Protocol"] = "cat" - resp, protocols = transact_http(req) - self.assertEqual(resp.status, 101) - self.assertEqual(protocols, ["cat"]) - self.assertEqual(resp.getheader("Sec-WebSocket-Protocol"), None) - - req = self.default_req() - req.headers["Sec-WebSocket-Protocol"] = "cat, base64" - resp, protocols = transact_http(req) - self.assertEqual(resp.status, 101) - self.assertEqual(protocols, ["cat", "base64"]) - self.assertEqual(resp.getheader("Sec-WebSocket-Protocol"), "base64") - -def read_frames(dec): - frames = [] - while True: - frame = dec.read_frame() - if frame is None: - break - frames.append((frame.fin, frame.opcode, frame.payload)) - return frames - -def read_messages(dec): - messages = [] - while True: - message = dec.read_message() - if message is None: - break - messages.append((message.opcode, message.payload)) - return messages - -class TestWebSocketDecoder(unittest.TestCase): - def test_rfc(self): - """Test samples from RFC 6455 section 5.7.""" - TESTS = [ - ("\x81\x05\x48\x65\x6c\x6c\x6f", False, - [(True, 1, "Hello")], - [(1, u"Hello")]), - ("\x81\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58", True, - [(True, 1, "Hello")], - [(1, u"Hello")]), - ("\x01\x03\x48\x65\x6c\x80\x02\x6c\x6f", False, - [(False, 1, "Hel"), (True, 0, "lo")], - [(1, u"Hello")]), - ("\x89\x05\x48\x65\x6c\x6c\x6f", False, - [(True, 9, "Hello")], - [(9, u"Hello")]), - ("\x8a\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58", True, - [(True, 10, "Hello")], - [(10, u"Hello")]), - ("\x82\x7e\x01\x00" + "\x00" * 256, False, - [(True, 2, "\x00" * 256)], - [(2, "\x00" * 256)]), - ("\x82\x7f\x00\x00\x00\x00\x00\x01\x00\x00" + "\x00" * 65536, False, - [(True, 2, "\x00" * 65536)], - [(2, "\x00" * 65536)]), - ("\x82\x7f\x00\x00\x00\x00\x00\x01\x00\x03" + "ABCD" * 16384 + "XYZ", False, - [(True, 2, "ABCD" * 16384 + "XYZ")], - [(2, "ABCD" * 16384 + "XYZ")]), - ] - for data, use_mask, expected_frames, expected_messages in TESTS: - dec = WebSocketDecoder(use_mask = use_mask) - dec.feed(data) - actual_frames = read_frames(dec) - self.assertEqual(actual_frames, expected_frames) - - dec = WebSocketDecoder(use_mask = use_mask) - dec.feed(data) - actual_messages = read_messages(dec) - self.assertEqual(actual_messages, expected_messages) - - dec = WebSocketDecoder(use_mask = not use_mask) - dec.feed(data) - self.assertRaises(WebSocketDecoder.MaskingError, dec.read_frame) - - def test_empty_feed(self): - """Test that the decoder can handle a zero-byte feed.""" - dec = WebSocketDecoder() - self.assertEqual(dec.read_frame(), None) - dec.feed("") - self.assertEqual(dec.read_frame(), None) - dec.feed("\x81\x05H") - self.assertEqual(dec.read_frame(), None) - dec.feed("ello") - self.assertEqual(read_frames(dec), [(True, 1, u"Hello")]) - - def test_empty_frame(self): - """Test that a frame may contain a zero-byte payload.""" - dec = WebSocketDecoder() - dec.feed("\x81\x00") - self.assertEqual(read_frames(dec), [(True, 1, u"")]) - dec.feed("\x82\x00") - self.assertEqual(read_frames(dec), [(True, 2, "")]) - - def test_empty_message(self): - """Test that a message may have a zero-byte payload.""" - dec = WebSocketDecoder() - dec.feed("\x01\x00\x00\x00\x80\x00") - self.assertEqual(read_messages(dec), [(1, u"")]) - dec.feed("\x02\x00\x00\x00\x80\x00") - self.assertEqual(read_messages(dec), [(2, "")]) - - def test_interleaved_control(self): - """Test that control messages interleaved with fragmented messages are - returned.""" - dec = WebSocketDecoder() - dec.feed("\x89\x04PING\x01\x03Hel\x8a\x04PONG\x80\x02lo\x89\x04PING") - self.assertEqual(read_messages(dec), [(9, "PING"), (10, "PONG"), (1, u"Hello"), (9, "PING")]) - - def test_fragmented_control(self): - """Test that illegal fragmented control messages cause an error.""" - dec = WebSocketDecoder() - dec.feed("\x09\x04PING") - self.assertRaises(ValueError, dec.read_message) - - def test_zero_opcode(self): - """Test that it is an error for the first frame in a message to have an - opcode of 0.""" - dec = WebSocketDecoder() - dec.feed("\x80\x05Hello") - self.assertRaises(ValueError, dec.read_message) - dec = WebSocketDecoder() - dec.feed("\x00\x05Hello") - self.assertRaises(ValueError, dec.read_message) - - def test_nonzero_opcode(self): - """Test that every frame after the first must have a zero opcode.""" - dec = WebSocketDecoder() - dec.feed("\x01\x01H\x01\x02el\x80\x02lo") - self.assertRaises(ValueError, dec.read_message) - dec = WebSocketDecoder() - dec.feed("\x01\x01H\x00\x02el\x01\x02lo") - self.assertRaises(ValueError, dec.read_message) - - def test_utf8(self): - """Test that text frames (opcode 1) are decoded from UTF-8.""" - text = u"Hello World or Καλημέρα κόσμε or こんにちは 世界 or \U0001f639" - utf8_text = text.encode("utf-8") - dec = WebSocketDecoder() - dec.feed("\x81" + chr(len(utf8_text)) + utf8_text) - self.assertEqual(read_messages(dec), [(1, text)]) - - def test_wrong_utf8(self): - """Test that failed UTF-8 decoding causes an error.""" - TESTS = [ - "\xc0\x41", # Non-shortest form. - "\xc2", # Unfinished sequence. - ] - for test in TESTS: - dec = WebSocketDecoder() - dec.feed("\x81" + chr(len(test)) + test) - self.assertRaises(ValueError, dec.read_message) - - def test_overly_large_payload(self): - """Test that large payloads are rejected.""" - dec = WebSocketDecoder() - dec.feed("\x82\x7f\x00\x00\x00\x00\x01\x00\x00\x00") - self.assertRaises(ValueError, dec.read_frame) - -class TestWebSocketEncoder(unittest.TestCase): - def test_length(self): - """Test that payload lengths are encoded using the smallest number of - bytes.""" - TESTS = [(0, 0), (125, 0), (126, 2), (65535, 2), (65536, 8)] - for length, encoded_length in TESTS: - enc = WebSocketEncoder(use_mask = False) - eframe = enc.encode_frame(2, "\x00" * length) - self.assertEqual(len(eframe), 1 + 1 + encoded_length + length) - enc = WebSocketEncoder(use_mask = True) - eframe = enc.encode_frame(2, "\x00" * length) - self.assertEqual(len(eframe), 1 + 1 + encoded_length + 4 + length) - - def test_roundtrip(self): - TESTS = [ - (1, u"Hello world"), - (1, u"Hello \N{WHITE SMILING FACE}"), - ] - for opcode, payload in TESTS: - for use_mask in (False, True): - enc = WebSocketEncoder(use_mask = use_mask) - enc_message = enc.encode_message(opcode, payload) - dec = WebSocketDecoder(use_mask = use_mask) - dec.feed(enc_message) - self.assertEqual(read_messages(dec), [(opcode, payload)]) - -def format_address(addr): - return "%s:%d" % addr - -class TestConnectionLimit(unittest.TestCase): - def setUp(self): - self.p = subprocess.Popen(["./flashproxy-client", format_address(LOCAL_ADDRESS), format_address(REMOTE_ADDRESS)]) - - def tearDown(self): - self.p.terminate() - -# def test_remote_limit(self): -# """Test that the client transport plugin limits the number of remote -# connections that it will accept.""" -# for i in range(5): -# s = socket.create_connection(REMOTE_ADDRESS, 2) -# self.assertRaises(socket.error, socket.create_connection, REMOTE_ADDRESS) - -if __name__ == "__main__": - unittest.main() diff --git a/flashproxy-client-test.py b/flashproxy-client-test.py new file mode 100755 index 0000000..0281f42 --- /dev/null +++ b/flashproxy-client-test.py @@ -0,0 +1,401 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import base64 +import cStringIO +import httplib +import socket +import subprocess +import sys +import unittest +try: + from hashlib import sha1 +except ImportError: + # Python 2.4 uses this name. + from sha import sha as sha1 + +# Special tricks to load a module whose filename contains a dash and doesn't end +# in ".py". +import imp +dont_write_bytecode = sys.dont_write_bytecode +sys.dont_write_bytecode = True +fp_client = imp.load_source("fp_client", "flashproxy-client") +parse_socks_request = fp_client.parse_socks_request +handle_websocket_request = fp_client.handle_websocket_request +WebSocketDecoder = fp_client.WebSocketDecoder +WebSocketEncoder = fp_client.WebSocketEncoder +sys.dont_write_bytecode = dont_write_bytecode +del dont_write_bytecode +del fp_client + +LOCAL_ADDRESS = ("127.0.0.1", 40000) +REMOTE_ADDRESS = ("127.0.0.1", 40001) + +class TestSocks(unittest.TestCase): + def test_parse_socks_request_empty(self): + self.assertRaises(ValueError, parse_socks_request, "") + def test_parse_socks_request_short(self): + self.assertRaises(ValueError, parse_socks_request, "\x04\x01\x99\x99\x01\x02\x03\x04") + def test_parse_socks_request_ip_userid_missing(self): + dest, port = parse_socks_request("\x04\x01\x99\x99\x01\x02\x03\x04\x00") + dest, port = parse_socks_request("\x04\x01\x99\x99\x01\x02\x03\x04\x00userid") + self.assertEqual((dest, port), ("1.2.3.4", 0x9999)) + def test_parse_socks_request_ip(self): + dest, port = parse_socks_request("\x04\x01\x99\x99\x01\x02\x03\x04userid\x00") + self.assertEqual((dest, port), ("1.2.3.4", 0x9999)) + def test_parse_socks_request_hostname_missing(self): + self.assertRaises(ValueError, parse_socks_request, "\x04\x01\x99\x99\x00\x00\x00\x01userid\x00") + self.assertRaises(ValueError, parse_socks_request, "\x04\x01\x99\x99\x00\x00\x00\x01userid\x00abc") + def test_parse_socks_request_hostname(self): + dest, port = parse_socks_request("\x04\x01\x99\x99\x00\x00\x00\x01userid\x00abc\x00") + +class DummySocket(object): + def __init__(self, read_fd, write_fd): + self.read_fd = read_fd + self.write_fd = write_fd + self.readp = 0 + + def read(self, *args, **kwargs): + self.read_fd.seek(self.readp, 0) + data = self.read_fd.read(*args, **kwargs) + self.readp = self.read_fd.tell() + return data + + def readline(self, *args, **kwargs): + self.read_fd.seek(self.readp, 0) + data = self.read_fd.readline(*args, **kwargs) + self.readp = self.read_fd.tell() + return data + + def recv(self, size, *args, **kwargs): + return self.read(size) + + def write(self, data): + self.write_fd.seek(0, 2) + self.write_fd.write(data) + + def send(self, data, *args, **kwargs): + return self.write(data) + + def sendall(self, data, *args, **kwargs): + return self.write(data) + + def makefile(self, *args, **kwargs): + return self + +def dummy_socketpair(): + f1 = cStringIO.StringIO() + f2 = cStringIO.StringIO() + return (DummySocket(f1, f2), DummySocket(f2, f1)) + +class HTTPRequest(object): + def __init__(self): + self.method = "GET" + self.path = "/" + self.headers = {} + +def transact_http(req): + l, r = dummy_socketpair() + r.send("%s %s HTTP/1.0\r\n" % (req.method, req.path)) + for k, v in req.headers.items(): + r.send("%s: %s\r\n" % (k, v)) + r.send("\r\n") + protocols = handle_websocket_request(l) + + resp = httplib.HTTPResponse(r) + resp.begin() + return resp, protocols + +class TestHandleWebSocketRequest(unittest.TestCase): + DEFAULT_KEY = "0123456789ABCDEF" + DEFAULT_KEY_BASE64 = base64.b64encode(DEFAULT_KEY) + MAGIC_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + + @staticmethod + def default_req(): + req = HTTPRequest() + req.method = "GET" + req.path = "/" + req.headers["Upgrade"] = "websocket" + req.headers["Connection"] = "Upgrade" + req.headers["Sec-WebSocket-Key"] = TestHandleWebSocketRequest.DEFAULT_KEY_BASE64 + req.headers["Sec-WebSocket-Version"] = "13" + + return req + + def assert_ok(self, req): + resp, protocols = transact_http(req) + self.assertEqual(resp.status, 101) + self.assertEqual(resp.getheader("Upgrade").lower(), "websocket") + self.assertEqual(resp.getheader("Connection").lower(), "upgrade") + self.assertEqual(resp.getheader("Sec-WebSocket-Accept"), base64.b64encode(sha1(self.DEFAULT_KEY_BASE64 + self.MAGIC_GUID).digest())) + self.assertEqual(protocols, []) + + def assert_not_ok(self, req): + resp, protocols = transact_http(req) + self.assertEqual(resp.status // 100, 4) + self.assertEqual(protocols, None) + + def test_default(self): + req = self.default_req() + self.assert_ok(req) + + def test_missing_upgrade(self): + req = self.default_req() + del req.headers["Upgrade"] + self.assert_not_ok(req) + + def test_missing_connection(self): + req = self.default_req() + del req.headers["Connection"] + self.assert_not_ok(req) + + def test_case_insensitivity(self): + """Test that the values of the Upgrade and Connection headers are + case-insensitive.""" + req = self.default_req() + req.headers["Upgrade"] = req.headers["Upgrade"].lower() + self.assert_ok(req) + req.headers["Upgrade"] = req.headers["Upgrade"].upper() + self.assert_ok(req) + req.headers["Connection"] = req.headers["Connection"].lower() + self.assert_ok(req) + req.headers["Connection"] = req.headers["Connection"].upper() + self.assert_ok(req) + + def test_bogus_key(self): + req = self.default_req() + req.headers["Sec-WebSocket-Key"] = base64.b64encode(self.DEFAULT_KEY[:-1]) + self.assert_not_ok(req) + + req.headers["Sec-WebSocket-Key"] = "///" + self.assert_not_ok(req) + + def test_versions(self): + req = self.default_req() + req.headers["Sec-WebSocket-Version"] = "13" + self.assert_ok(req) + req.headers["Sec-WebSocket-Version"] = "8" + self.assert_ok(req) + + req.headers["Sec-WebSocket-Version"] = "7" + self.assert_not_ok(req) + req.headers["Sec-WebSocket-Version"] = "9" + self.assert_not_ok(req) + + del req.headers["Sec-WebSocket-Version"] + self.assert_not_ok(req) + + def test_protocols(self): + req = self.default_req() + req.headers["Sec-WebSocket-Protocol"] = "base64" + resp, protocols = transact_http(req) + self.assertEqual(resp.status, 101) + self.assertEqual(protocols, ["base64"]) + self.assertEqual(resp.getheader("Sec-WebSocket-Protocol"), "base64") + + req = self.default_req() + req.headers["Sec-WebSocket-Protocol"] = "cat" + resp, protocols = transact_http(req) + self.assertEqual(resp.status, 101) + self.assertEqual(protocols, ["cat"]) + self.assertEqual(resp.getheader("Sec-WebSocket-Protocol"), None) + + req = self.default_req() + req.headers["Sec-WebSocket-Protocol"] = "cat, base64" + resp, protocols = transact_http(req) + self.assertEqual(resp.status, 101) + self.assertEqual(protocols, ["cat", "base64"]) + self.assertEqual(resp.getheader("Sec-WebSocket-Protocol"), "base64") + +def read_frames(dec): + frames = [] + while True: + frame = dec.read_frame() + if frame is None: + break + frames.append((frame.fin, frame.opcode, frame.payload)) + return frames + +def read_messages(dec): + messages = [] + while True: + message = dec.read_message() + if message is None: + break + messages.append((message.opcode, message.payload)) + return messages + +class TestWebSocketDecoder(unittest.TestCase): + def test_rfc(self): + """Test samples from RFC 6455 section 5.7.""" + TESTS = [ + ("\x81\x05\x48\x65\x6c\x6c\x6f", False, + [(True, 1, "Hello")], + [(1, u"Hello")]), + ("\x81\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58", True, + [(True, 1, "Hello")], + [(1, u"Hello")]), + ("\x01\x03\x48\x65\x6c\x80\x02\x6c\x6f", False, + [(False, 1, "Hel"), (True, 0, "lo")], + [(1, u"Hello")]), + ("\x89\x05\x48\x65\x6c\x6c\x6f", False, + [(True, 9, "Hello")], + [(9, u"Hello")]), + ("\x8a\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58", True, + [(True, 10, "Hello")], + [(10, u"Hello")]), + ("\x82\x7e\x01\x00" + "\x00" * 256, False, + [(True, 2, "\x00" * 256)], + [(2, "\x00" * 256)]), + ("\x82\x7f\x00\x00\x00\x00\x00\x01\x00\x00" + "\x00" * 65536, False, + [(True, 2, "\x00" * 65536)], + [(2, "\x00" * 65536)]), + ("\x82\x7f\x00\x00\x00\x00\x00\x01\x00\x03" + "ABCD" * 16384 + "XYZ", False, + [(True, 2, "ABCD" * 16384 + "XYZ")], + [(2, "ABCD" * 16384 + "XYZ")]), + ] + for data, use_mask, expected_frames, expected_messages in TESTS: + dec = WebSocketDecoder(use_mask = use_mask) + dec.feed(data) + actual_frames = read_frames(dec) + self.assertEqual(actual_frames, expected_frames) + + dec = WebSocketDecoder(use_mask = use_mask) + dec.feed(data) + actual_messages = read_messages(dec) + self.assertEqual(actual_messages, expected_messages) + + dec = WebSocketDecoder(use_mask = not use_mask) + dec.feed(data) + self.assertRaises(WebSocketDecoder.MaskingError, dec.read_frame) + + def test_empty_feed(self): + """Test that the decoder can handle a zero-byte feed.""" + dec = WebSocketDecoder() + self.assertEqual(dec.read_frame(), None) + dec.feed("") + self.assertEqual(dec.read_frame(), None) + dec.feed("\x81\x05H") + self.assertEqual(dec.read_frame(), None) + dec.feed("ello") + self.assertEqual(read_frames(dec), [(True, 1, u"Hello")]) + + def test_empty_frame(self): + """Test that a frame may contain a zero-byte payload.""" + dec = WebSocketDecoder() + dec.feed("\x81\x00") + self.assertEqual(read_frames(dec), [(True, 1, u"")]) + dec.feed("\x82\x00") + self.assertEqual(read_frames(dec), [(True, 2, "")]) + + def test_empty_message(self): + """Test that a message may have a zero-byte payload.""" + dec = WebSocketDecoder() + dec.feed("\x01\x00\x00\x00\x80\x00") + self.assertEqual(read_messages(dec), [(1, u"")]) + dec.feed("\x02\x00\x00\x00\x80\x00") + self.assertEqual(read_messages(dec), [(2, "")]) + + def test_interleaved_control(self): + """Test that control messages interleaved with fragmented messages are + returned.""" + dec = WebSocketDecoder() + dec.feed("\x89\x04PING\x01\x03Hel\x8a\x04PONG\x80\x02lo\x89\x04PING") + self.assertEqual(read_messages(dec), [(9, "PING"), (10, "PONG"), (1, u"Hello"), (9, "PING")]) + + def test_fragmented_control(self): + """Test that illegal fragmented control messages cause an error.""" + dec = WebSocketDecoder() + dec.feed("\x09\x04PING") + self.assertRaises(ValueError, dec.read_message) + + def test_zero_opcode(self): + """Test that it is an error for the first frame in a message to have an + opcode of 0.""" + dec = WebSocketDecoder() + dec.feed("\x80\x05Hello") + self.assertRaises(ValueError, dec.read_message) + dec = WebSocketDecoder() + dec.feed("\x00\x05Hello") + self.assertRaises(ValueError, dec.read_message) + + def test_nonzero_opcode(self): + """Test that every frame after the first must have a zero opcode.""" + dec = WebSocketDecoder() + dec.feed("\x01\x01H\x01\x02el\x80\x02lo") + self.assertRaises(ValueError, dec.read_message) + dec = WebSocketDecoder() + dec.feed("\x01\x01H\x00\x02el\x01\x02lo") + self.assertRaises(ValueError, dec.read_message) + + def test_utf8(self): + """Test that text frames (opcode 1) are decoded from UTF-8.""" + text = u"Hello World or Καλημέρα κόσμε or こんにちは 世界 or \U0001f639" + utf8_text = text.encode("utf-8") + dec = WebSocketDecoder() + dec.feed("\x81" + chr(len(utf8_text)) + utf8_text) + self.assertEqual(read_messages(dec), [(1, text)]) + + def test_wrong_utf8(self): + """Test that failed UTF-8 decoding causes an error.""" + TESTS = [ + "\xc0\x41", # Non-shortest form. + "\xc2", # Unfinished sequence. + ] + for test in TESTS: + dec = WebSocketDecoder() + dec.feed("\x81" + chr(len(test)) + test) + self.assertRaises(ValueError, dec.read_message) + + def test_overly_large_payload(self): + """Test that large payloads are rejected.""" + dec = WebSocketDecoder() + dec.feed("\x82\x7f\x00\x00\x00\x00\x01\x00\x00\x00") + self.assertRaises(ValueError, dec.read_frame) + +class TestWebSocketEncoder(unittest.TestCase): + def test_length(self): + """Test that payload lengths are encoded using the smallest number of + bytes.""" + TESTS = [(0, 0), (125, 0), (126, 2), (65535, 2), (65536, 8)] + for length, encoded_length in TESTS: + enc = WebSocketEncoder(use_mask = False) + eframe = enc.encode_frame(2, "\x00" * length) + self.assertEqual(len(eframe), 1 + 1 + encoded_length + length) + enc = WebSocketEncoder(use_mask = True) + eframe = enc.encode_frame(2, "\x00" * length) + self.assertEqual(len(eframe), 1 + 1 + encoded_length + 4 + length) + + def test_roundtrip(self): + TESTS = [ + (1, u"Hello world"), + (1, u"Hello \N{WHITE SMILING FACE}"), + ] + for opcode, payload in TESTS: + for use_mask in (False, True): + enc = WebSocketEncoder(use_mask = use_mask) + enc_message = enc.encode_message(opcode, payload) + dec = WebSocketDecoder(use_mask = use_mask) + dec.feed(enc_message) + self.assertEqual(read_messages(dec), [(opcode, payload)]) + +def format_address(addr): + return "%s:%d" % addr + +class TestConnectionLimit(unittest.TestCase): + def setUp(self): + self.p = subprocess.Popen(["./flashproxy-client", format_address(LOCAL_ADDRESS), format_address(REMOTE_ADDRESS)]) + + def tearDown(self): + self.p.terminate() + +# def test_remote_limit(self): +# """Test that the client transport plugin limits the number of remote +# connections that it will accept.""" +# for i in range(5): +# s = socket.create_connection(REMOTE_ADDRESS, 2) +# self.assertRaises(socket.error, socket.create_connection, REMOTE_ADDRESS) + +if __name__ == "__main__": + unittest.main() diff --git a/setup-client-exe.py b/setup-client-exe.py index 5baf71d..62b9c87 100755 --- a/setup-client-exe.py +++ b/setup-client-exe.py @@ -1,4 +1,5 @@ #!/usr/bin/python +"""Setup file for the flashproxy-common python module.""" from distutils.core import setup import os import py2exe diff --git a/setup-common.py b/setup-common.py index 44c9c3e..85bb325 100755 --- a/setup-common.py +++ b/setup-common.py @@ -1,4 +1,24 @@ #!/usr/bin/env python +"""Setup file for the flashproxy-common python module. + +To build/install a self-contained binary distribution of flashproxy-client +(which integrates this module within it), see Makefile. +""" +# Note to future developers: +# +# We place flashproxy-common in the same directory as flashproxy-client for +# convenience, so that it's possible to run the client programs directly from +# a source checkout without needing to set PYTHONPATH. This works OK currently +# because flashproxy-client does not contain python modules, only programs, and +# therefore doesn't conflict with the flashproxy-common module. +# +# If we ever need to have a python module specific to flashproxy-client, the +# natural thing would be to add a setup.py for it. That is the reason why this +# file is called setup-common.py instead. However, there are still issues that +# arise from having two setup*.py files in the same directory, which is an +# unfortunate limitation of python's setuptools. +# +# See discussion on #6810 for more details.
import sys