
commit 5d3ae963fa6b514c28e5ad37e60a165d82526831 Author: Ximin Luo <infinity0@gmx.com> Date: Wed Nov 13 16:34:31 2013 +0000 move fac.py and associated tests to flashproxy-common - use the facilitator's parse_addr_spec, and fix client uses to set defhost = "" - the old client side accepted an empty host when defhost = None. - the facilitator side requires setting defhost = "" to accept an empty host - behaviour is otherwise equivalent --- Makefile | 2 +- facilitator/HACKING | 26 ++ facilitator/INSTALL | 15 +- facilitator/Makefile.am | 10 +- facilitator/doc/facilitator-design.txt | 3 - facilitator/fac.py | 404 -------------------------------- facilitator/facilitator | 22 +- facilitator/facilitator-email-poller | 10 +- facilitator/facilitator-reg-daemon | 16 +- facilitator/facilitator-test.py | 143 +---------- facilitator/facilitator.cgi | 2 +- flashproxy-client | 12 +- flashproxy-reg-appspot | 2 +- flashproxy-reg-email | 2 +- flashproxy-reg-url | 2 +- flashproxy/fac.py | 224 ++++++++++++++++++ flashproxy/proc.py | 47 ++++ flashproxy/reg.py | 31 +++ flashproxy/test/test_fac.py | 93 ++++++++ flashproxy/test/test_reg.py | 23 ++ flashproxy/test/test_util.py | 35 +++ flashproxy/util.py | 92 ++++++-- 22 files changed, 618 insertions(+), 598 deletions(-) diff --git a/Makefile b/Makefile index 9e8d2dd..93287f6 100644 --- a/Makefile +++ b/Makefile @@ -84,7 +84,7 @@ test-full: test cd facilitator && \ { test -x ./config.status && ./config.status || \ { test -x ./configure || ./autogen.sh; } && ./configure; } \ - && make && make check + && make && PYTHONPATH=.. make check cd proxy && make test force-dist: diff --git a/facilitator/HACKING b/facilitator/HACKING new file mode 100644 index 0000000..f102880 --- /dev/null +++ b/facilitator/HACKING @@ -0,0 +1,26 @@ +== Running from source checkout + +In order to run the code directly from a source checkout, you must make sure it +can find the flashproxy module, located in the top-level directory of the +source checkout, which is probably the parent directory. You have two options: + +1. Install it in "development mode", see [1] + + flashproxy# python setup-common.py develop + +This process is reversible too: + + flashproxy# python setup-common.py develop --uninstall + +The disadvantage is that other programs (such as a system-installed flashproxy, +or other checkouts in another directory) will see this development copy, rather +than a more appropriate copy. + +2. Export PYTHONPATH when you need to run + + $ export PYTHONPATH=.. + $ make check + +The disadvantage is that you need to do this every shell session. + +[1] http://pythonhosted.org/distribute/setuptools.html#development-mode diff --git a/facilitator/INSTALL b/facilitator/INSTALL index 6325e53..7716ccc 100644 --- a/facilitator/INSTALL +++ b/facilitator/INSTALL @@ -1,12 +1,21 @@ Install the dependencies. - $ apt-get install make openssl python-m2crypto - $ apt-get install automake autoconf # if running from git - $ apt-get install apache2 + # apt-get install make openssl python-m2crypto + # apt-get install automake autoconf # if running from git + + # apt-get install apache2 You may use a different webserver, but currently we only provide an apache2 site config example, so you will need to adapt this to the correct syntax. + # apt-get install flashproxy-common + +If your distro does not have flashproxy-common, you can install it +directly from the top-level source directory: + + flashproxy# python setup-common.py install --record install.log \ + --single-version-externally-managed + Configure and install. $ ./autogen.sh # if running from git or ./configure doesn't otherwise exist diff --git a/facilitator/Makefile.am b/facilitator/Makefile.am index 2d315b9..435956d 100644 --- a/facilitator/Makefile.am +++ b/facilitator/Makefile.am @@ -2,9 +2,7 @@ fpfacilitatoruser = @fpfacilitatoruser@ initconfdir = @initconfdir@ -# TODO(infinity0): switch this to @cgibindir@ once we replace fac.py with -# flashproxy-common, so that we install facilitator.cgi in the right place -cgibindir = @bindir@ +cgibindir = @cgibindir@ # unfortunately sysvinit does not support having initscripts in /usr/local/etc # yet, so we have to hard code a path here. :( @@ -16,7 +14,7 @@ appengineconfdir = $(pkgconfdir)/reg-appspot # automake PLVs -dist_bin_SCRIPTS = facilitator facilitator-email-poller facilitator-reg-daemon facilitator-reg fac.py +dist_bin_SCRIPTS = facilitator facilitator-email-poller facilitator-reg-daemon facilitator-reg dist_cgibin_SCRIPTS = facilitator.cgi if DO_INITSCRIPTS initscript_SCRIPTS = init.d/facilitator init.d/facilitator-email-poller init.d/facilitator-reg-daemon @@ -29,13 +27,13 @@ pkgconf_DATA = examples/facilitator-relays dist_appengine_DATA = appengine/app.yaml appengine/config.go appengine/fp-reg.go appengineconf_DATA = appengine/config.go CLEANFILES = examples/fp-facilitator.conf -EXTRA_DIST = examples/fp-facilitator.conf.in $(TESTS) +EXTRA_DIST = examples/fp-facilitator.conf.in HACKING $(TESTS) TESTS = facilitator-test.py # see http://www.gnu.org/software/automake/manual/html_node/Parallel-Test-Harness.... TEST_EXTENSIONS = .py PY_LOG_COMPILER = $(PYTHON) -AM_TESTS_ENVIRONMENT = PYTHONPATH='$(srcdir)'; export PYTHONPATH; +AM_TESTS_ENVIRONMENT = PYTHONPATH='$(srcdir):$(PYTHONPATH)'; export PYTHONPATH; AM_PY_LOG_FLAGS = # AC_CONFIG_FILES doesn't fully-expand directory variables diff --git a/facilitator/doc/facilitator-design.txt b/facilitator/doc/facilitator-design.txt index 0d84da3..20f9c0a 100644 --- a/facilitator/doc/facilitator-design.txt +++ b/facilitator/doc/facilitator-design.txt @@ -39,6 +39,3 @@ the HTTP method, either yours or that of another facilitator. It takes advantage of the fact that a censor cannot distinguish between a TLS connection to appspot.com or google.com, since the IPs are the same, and it is highly unlikely that anyone will try to block the latter. - -fac.py is a Python module containing code common to the various -facilitator programs. diff --git a/facilitator/fac.py b/facilitator/fac.py deleted file mode 100644 index 00c103c..0000000 --- a/facilitator/fac.py +++ /dev/null @@ -1,404 +0,0 @@ -import errno -import os -import re -import socket -import stat -import subprocess -import pwd -import urlparse -from collections import namedtuple - -DEFAULT_CLIENT_TRANSPORT = "websocket" - -# Return true iff the given fd is readable, writable, and executable only by its -# owner. -def check_perms(fd): - mode = os.fstat(fd)[0] - return (mode & (stat.S_IRWXG | stat.S_IRWXO)) == 0 - -# Drop privileges by switching ID to that of the given user. -# http://stackoverflow.com/questions/2699907/dropping-root-permissions-in-pyth... -# https://www.securecoding.cert.org/confluence/display/seccode/POS36-C.+Observ... -# https://www.securecoding.cert.org/confluence/display/seccode/POS37-C.+Ensure... -def drop_privs(username): - uid = pwd.getpwnam(username).pw_uid - gid = pwd.getpwnam(username).pw_gid - os.setgroups([]) - os.setgid(gid) - os.setuid(uid) - try: - os.setuid(0) - except OSError: - pass - else: - raise AssertionError("setuid(0) succeeded after attempting to drop privileges") - -# A decorator to ignore "broken pipe" errors. -def catch_epipe(fn): - def ret(self, *args): - try: - return fn(self, *args) - except socket.error, e: - try: - err_num = e.errno - except AttributeError: - # Before Python 2.6, exception can be a pair. - err_num, errstr = e - except: - raise - if err_num != errno.EPIPE: - raise - return ret - -def parse_addr_spec(spec, defhost = None, defport = None): - """Parse a host:port specification and return a 2-tuple ("host", port) as - understood by the Python socket functions. - >>> parse_addr_spec("192.168.0.1:9999") - ('192.168.0.1', 9999) - - If defhost or defport are given, those parts of the specification may be - omitted; if so, they will be filled in with defaults. - >>> parse_addr_spec("192.168.0.2:8888", defhost="192.168.0.1", defport=9999) - ('192.168.0.2', 8888) - >>> parse_addr_spec(":8888", defhost="192.168.0.1", defport=9999) - ('192.168.0.1', 8888) - >>> parse_addr_spec("192.168.0.2", defhost="192.168.0.1", defport=9999) - ('192.168.0.2', 9999) - >>> parse_addr_spec("192.168.0.2:", defhost="192.168.0.1", defport=9999) - ('192.168.0.2', 9999) - >>> parse_addr_spec(":", defhost="192.168.0.1", defport=9999) - ('192.168.0.1', 9999) - >>> parse_addr_spec("", defhost="192.168.0.1", defport=9999) - ('192.168.0.1', 9999) - IPv6 addresses must be enclosed in square brackets.""" - host = None - port = None - af = 0 - m = None - # IPv6 syntax. - if not m: - m = re.match(ur'^\[(.+)\]:(\d*)$', spec) - if m: - host, port = m.groups() - af = socket.AF_INET6 - if not m: - m = re.match(ur'^\[(.+)\]$', spec) - if m: - host, = m.groups() - af = socket.AF_INET6 - # IPv4/hostname/port-only syntax. - if not m: - try: - host, port = spec.split(":", 1) - except ValueError: - host = spec - if re.match(ur'^[\d.]+$', host): - af = socket.AF_INET - else: - af = 0 - host = host or defhost - port = port or defport - if host is None or port is None: - raise ValueError("Bad address specification \"%s\"" % spec) - return host, int(port) - -def resolve_to_ip(host, port, af=0, gai_flags=0): - """Resolves a host string to an IP address in canonical format. - - Note: in many cases this is not necessary since the consumer of the address - can probably accept host names directly. - - :param: host string to resolve; may be a DNS name or an IP address. - :param: port of the host - :param: af address family, default unspecified. set to socket.AF_INET or - socket.AF_INET6 to force IPv4 or IPv6 name resolution. - :returns: (IP address in canonical format, port) - """ - # Forward-resolve the name into an addrinfo struct. Real DNS resolution is - # done only if resolve is true; otherwise the address must be numeric. - try: - addrs = socket.getaddrinfo(host, port, af, 0, 0, gai_flags) - except socket.gaierror, e: - raise ValueError("Bad host or port: \"%s\" \"%s\": %s" % (host, port, str(e))) - if not addrs: - raise ValueError("Bad host or port: \"%s\" \"%s\"" % (host, port)) - - # Convert the result of socket.getaddrinfo (which is a 2-tuple for IPv4 and - # a 4-tuple for IPv6) into a (host, port) 2-tuple. - host, port = socket.getnameinfo(addrs[0][4], socket.NI_NUMERICHOST | socket.NI_NUMERICSERV) - return host, int(port) - -def canonical_ip(host, port, af=0): - """Convert an IP address to a canonical format. Identical to resolve_to_ip, - except that the host param must already be an IP address.""" - return resolve_to_ip(host, port, af, gai_flags=socket.AI_NUMERICHOST) - -def format_addr(addr): - host, port = addr - host_str = u"" - port_str = u"" - if host is not None: - # Numeric IPv6 address? - try: - addrs = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM, socket.IPPROTO_TCP, socket.AI_NUMERICHOST) - af = addrs[0][0] - except socket.gaierror, e: - af = 0 - if af == socket.AF_INET6: - host_str = u"[%s]" % host - else: - host_str = u"%s" % host - if port is not None: - if not (0 < port <= 65535): - raise ValueError("port must be between 1 and 65535 (is %d)" % port) - port_str = u":%d" % port - - if not host_str and not port_str: - raise ValueError("host and port may not both be None") - return u"%s%s" % (host_str, port_str) - -def read_client_registrations(body, defhost=None, defport=None): - """Yield client registrations (as Endpoints) from an encoded registration - message body. The message format is one registration per line, with each - line being encoded as application/x-www-form-urlencoded. The key "client" is - required and contains the client address and port (perhaps filled in by - defhost and defport). The key "client-transport" is optional and defaults to - "websocket". - Example: - client=1.2.3.4:9000&client-transport=websocket - client=1.2.3.4:9090&client-transport=obfs3|websocket - """ - for line in body.splitlines(): - qs = urlparse.parse_qs(line, keep_blank_values=True, strict_parsing=True) - # Get the unique value associated with the given key in qs. If the key - # is absent or appears more than once, raise ValueError. - def get_unique(key, default=None): - try: - vals = qs[key] - except KeyError: - if default is None: - raise ValueError("missing %r key" % key) - vals = (default,) - if len(vals) != 1: - raise ValueError("more than one %r key" % key) - return vals[0] - addr = parse_addr_spec(get_unique("client"), defhost, defport) - transport = get_unique("client-transport", DEFAULT_CLIENT_TRANSPORT) - yield Endpoint(addr, transport) - - -class Transport(namedtuple("Transport", "inner outer")): - @classmethod - def parse(cls, transport): - if isinstance(transport, cls): - return transport - elif type(transport) == str: - if "|" in transport: - inner, outer = transport.rsplit("|", 1) - else: - inner, outer = "", transport - return cls(inner, outer) - else: - raise ValueError("could not parse transport: %s" % transport) - - def __init__(self, inner, outer): - if not outer: - raise ValueError("outer (proxy) part of transport must be non-empty: %s" % str(self)) - - def __str__(self): - return "%s|%s" % (self.inner, self.outer) if self.inner else self.outer - - -class Endpoint(namedtuple("Endpoint", "addr transport")): - @classmethod - def parse(cls, spec, transport, defhost = None, defport = None): - host, port = parse_addr_spec(spec, defhost, defport) - return cls((host, port), Transport.parse(transport)) - - -def skip_space(pos, line): - """Skip a (possibly empty) sequence of space characters (the ASCII character - '\x20' exactly). Returns a pair (pos, num_skipped).""" - begin = pos - while pos < len(line) and line[pos] == "\x20": - pos += 1 - return pos, pos - begin - -TOKEN_CHARS = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-") -def get_token(pos, line): - begin = pos - while pos < len(line) and line[pos] in TOKEN_CHARS: - pos += 1 - if begin == pos: - raise ValueError("No token found at position %d" % pos) - return pos, line[begin:pos] - -def get_quoted_string(pos, line): - chars = [] - if not (pos < len(line) and line[pos] == '"'): - raise ValueError("Expected '\"' at beginning of quoted string.") - pos += 1 - while pos < len(line) and line[pos] != '"': - if line[pos] == '\\': - pos += 1 - if not (pos < len(line)): - raise ValueError("End of line after backslash in quoted string") - chars.append(line[pos]) - pos += 1 - if not (pos < len(line) and line[pos] == '"'): - raise ValueError("Expected '\"' at end of quoted string.") - pos += 1 - return pos, "".join(chars) - -def parse_transaction(line): - """A transaction is a command followed by zero or more key-value pairs. Like so: - COMMAND KEY="VALUE" KEY="\"ESCAPED\" VALUE" - Values must be quoted. Any byte value may be escaped with a backslash. - Returns a pair: (COMMAND, ((KEY1, VALUE1), (KEY2, VALUE2), ...)). - """ - pos = 0 - pos, skipped = skip_space(pos, line) - pos, command = get_token(pos, line) - - pairs = [] - while True: - pos, skipped = skip_space(pos, line) - if not (pos < len(line)): - break - if skipped == 0: - raise ValueError("Expected space before key-value pair") - pos, key = get_token(pos, line) - if not (pos < len(line) and line[pos] == '='): - raise ValueError("No '=' found after key") - pos += 1 - pos, value = get_quoted_string(pos, line) - pairs.append((key, value)) - return command, tuple(pairs) - -def param_first(key, params): - """Search 'params' for 'key' and return the first value that - occurs. If 'key' was not found, return None.""" - for k, v in params: - if key == k: - return v - return None - -def param_getlist(key, params): - """Search 'params' for 'key' and return a list with its values. If - 'key' did not appear in 'params', return the empty list.""" - result = [] - for k, v in params: - if key == k: - result.append(v) - return result - -def quote_string(s): - chars = [] - for c in s: - if c == "\\": - c = "\\\\" - elif c == "\"": - c = "\\\"" - chars.append(c) - return "\"" + "".join(chars) + "\"" - -def render_transaction(command, *params): - parts = [command] - for key, value in params: - parts.append("%s=%s" % (key, quote_string(value))) - return " ".join(parts) - -def fac_socket(facilitator_addr): - return socket.create_connection(facilitator_addr, 1.0).makefile() - -def transact(f, command, *params): - transaction = render_transaction(command, *params) - print >> f, transaction - f.flush() - line = f.readline() - if not (len(line) > 0 and line[-1] == '\n'): - raise ValueError("No newline at end of string returned by facilitator") - return parse_transaction(line[:-1]) - -def put_reg(facilitator_addr, client_addr, transport): - """Send a registration to the facilitator using a one-time socket. Returns - true iff the command was successful. transport is a transport string such as - "websocket" or "obfs3|websocket".""" - f = fac_socket(facilitator_addr) - params = [("CLIENT", format_addr(client_addr))] - params.append(("TRANSPORT", transport)) - try: - command, params = transact(f, "PUT", *params) - finally: - f.close() - return command == "OK" - -def get_reg(facilitator_addr, proxy_addr, proxy_transport_list): - """ - Get a client registration for proxy proxy_addr from the - facilitator at facilitator_addr using a one-time - socket. proxy_transport_list is a list containing the transport names that - the flashproxy supports. - - Returns a dict with keys "client", "client-transport", "relay", - and "relay-transport" if successful, or a dict with the key "client" - mapped to the value "" if there are no registrations available for - proxy_addr. Raises an exception otherwise.""" - f = fac_socket(facilitator_addr) - - # Form a list (in transact() format) with the transports that we - # should send to the facilitator. Then pass that list to the - # transact() function. - # For example, PROXY-TRANSPORT=obfs2 PROXY-TRANSPORT=obfs3. - transports = [("PROXY-TRANSPORT", tp) for tp in proxy_transport_list] - - try: - command, params = transact(f, "GET", ("FROM", format_addr(proxy_addr)), *transports) - finally: - f.close() - response = {} - check_back_in = param_first("CHECK-BACK-IN", params) - if check_back_in is not None: - try: - float(check_back_in) - except ValueError: - raise ValueError("Facilitator returned non-numeric polling interval.") - response["check-back-in"] = check_back_in - if command == "NONE": - response["client"] = "" - return response - elif command == "OK": - client_spec = param_first("CLIENT", params) - client_transport = param_first("CLIENT-TRANSPORT", params) - relay_spec = param_first("RELAY", params) - relay_transport = param_first("RELAY-TRANSPORT", params) - if not client_spec: - raise ValueError("Facilitator did not return CLIENT") - if not client_transport: - raise ValueError("Facilitator did not return CLIENT-TRANSPORT") - if not relay_spec: - raise ValueError("Facilitator did not return RELAY") - if not relay_transport: - raise ValueError("Facilitator did not return RELAY-TRANSPORT") - # Check the syntax returned by the facilitator. - client = parse_addr_spec(client_spec) - relay = parse_addr_spec(relay_spec) - response["client"] = format_addr(client) - response["client-transport"] = client_transport - response["relay"] = format_addr(relay) - response["relay-transport"] = relay_transport - return response - else: - raise ValueError("Facilitator response was not \"OK\"") - -def put_reg_base64(b64): - """Attempt to add a registration by running a facilitator-reg program - locally.""" - # Padding is optional, but the python base64 functions can't - # handle lack of padding. Add it here. Assumes correct encoding. - mod = len(b64) % 4 - if mod != 0: - b64 += (4 - mod) * "=" - p = subprocess.Popen(["facilitator-reg"], stdin=subprocess.PIPE) - stdout, stderr = p.communicate(b64) - return p.returncode == 0 diff --git a/facilitator/facilitator b/facilitator/facilitator index a2dd56a..39ffe2a 100755 --- a/facilitator/facilitator +++ b/facilitator/facilitator @@ -9,8 +9,10 @@ import threading import time from collections import defaultdict -import fac -from fac import Transport, Endpoint +from flashproxy import fac +from flashproxy import proc +from flashproxy.reg import Transport, Endpoint +from flashproxy.util import parse_addr_spec, format_addr, canonical_ip LISTEN_ADDRESS = "127.0.0.1" DEFAULT_LISTEN_PORT = 9002 @@ -241,7 +243,7 @@ class Handler(SocketServer.StreamRequestHandler): if buflen >= READLINE_MAX_LENGTH: raise socket.error("readline: refusing to buffer %d bytes (last read was %d bytes)" % (buflen, len(data))) - @fac.catch_epipe + @proc.catch_epipe def handle(self): num_lines = 0 while True: @@ -290,7 +292,7 @@ class Handler(SocketServer.StreamRequestHandler): if proxy_spec is None: return self.error(u"GET missing FROM param") try: - proxy_addr = fac.canonical_ip(*fac.parse_addr_spec(proxy_spec, defport=0)) + proxy_addr = canonical_ip(*parse_addr_spec(proxy_spec, defport=0)) except ValueError, e: return self.error(u"syntax error in proxy address %s: %s" % (safe_str(repr(proxy_spec)), safe_str(repr(str(e))))) @@ -309,9 +311,9 @@ class Handler(SocketServer.StreamRequestHandler): log(u"proxy (%s) gets client '%s' (supported transports: %s) (num relays: %s) (remaining regs: %d/%d)" % (safe_str(repr(proxy_spec)), safe_str(repr(client_reg.addr)), transport_list, num_relays(), num_unhandled_regs(), num_regs())) print >> self.wfile, fac.render_transaction("OK", - ("CLIENT", fac.format_addr(client_reg.addr)), + ("CLIENT", format_addr(client_reg.addr)), ("CLIENT-TRANSPORT", client_reg.transport.outer), - ("RELAY", fac.format_addr(relay_reg.addr)), + ("RELAY", format_addr(relay_reg.addr)), ("RELAY-TRANSPORT", relay_reg.transport.outer), ("CHECK-BACK-IN", str(check_back_in))) else: @@ -356,7 +358,7 @@ class Handler(SocketServer.StreamRequestHandler): self.send_ok() return True - finish = fac.catch_epipe(SocketServer.StreamRequestHandler.finish) + finish = proc.catch_epipe(SocketServer.StreamRequestHandler.finish) class Server(SocketServer.ThreadingMixIn, SocketServer.TCPServer): allow_reuse_address = True @@ -422,7 +424,7 @@ def parse_relay_file(servers, fp): transport_spec, addr_spec = line.strip().split() except ValueError, e: raise ValueError("Wrong line format: %s." % repr(line)) - addr = fac.parse_addr_spec(addr_spec, defport=DEFAULT_RELAY_PORT) + addr = parse_addr_spec(addr_spec, defport=DEFAULT_RELAY_PORT) transport = Transport.parse(transport_spec) if transport.outer not in options.outer_transports: raise ValueError(u"Unrecognized transport: %s" % transport) @@ -495,7 +497,7 @@ obfs2|websocket 1.4.6.1:4123\ server = Server(addrinfo[4], Handler) - log(u"start on %s" % fac.format_addr(addrinfo[4])) + log(u"start on %s" % format_addr(addrinfo[4])) log(u"using IPv4 relays %s" % str(RELAYS[socket.AF_INET]._endpoints)) log(u"using IPv6 relays %s" % str(RELAYS[socket.AF_INET6]._endpoints)) @@ -512,7 +514,7 @@ obfs2|websocket 1.4.6.1:4123\ if options.privdrop_username is not None: log(u"dropping privileges to those of user %s" % options.privdrop_username) try: - fac.drop_privs(options.privdrop_username) + proc.drop_privs(options.privdrop_username) except BaseException, e: print >> sys.stderr, "Can't drop privileges:", str(e) sys.exit(1) diff --git a/facilitator/facilitator-email-poller b/facilitator/facilitator-email-poller index 2efceec..0eef115 100755 --- a/facilitator/facilitator-email-poller +++ b/facilitator/facilitator-email-poller @@ -16,7 +16,9 @@ import sys import tempfile import time -import fac +from flashproxy import fac +from flashproxy import proc +from flashproxy.util import parse_addr_spec from hashlib import sha1 from M2Crypto import SSL @@ -209,7 +211,7 @@ Failed to open password file "%s": %s.\ """ % (options.password_filename, str(e)) sys.exit(1) try: - if not fac.check_perms(password_file.fileno()): + if not proc.check_perms(password_file.fileno()): print >> sys.stderr, "Refusing to run with group- or world-readable password file. Try" print >> sys.stderr, "\tchmod 600 %s" % options.password_filename sys.exit(1) @@ -221,7 +223,7 @@ try: if not res: raise ValueError("could not find email or password on line %s" % (lineno0+1)) (imap_addr_spec, email_addr, email_password) = res.groups() - imap_addr = fac.parse_addr_spec( + imap_addr = parse_addr_spec( imap_addr_spec or "", DEFAULT_IMAP_HOST, DEFAULT_IMAP_PORT) break else: @@ -255,7 +257,7 @@ if options.daemonize: if options.privdrop_username is not None: log(u"dropping privileges to those of user %s" % options.privdrop_username) try: - fac.drop_privs(options.privdrop_username) + proc.drop_privs(options.privdrop_username) except BaseException, e: print >> sys.stderr, "Can't drop privileges:", str(e) sys.exit(1) diff --git a/facilitator/facilitator-reg-daemon b/facilitator/facilitator-reg-daemon index cab7403..f5e592f 100755 --- a/facilitator/facilitator-reg-daemon +++ b/facilitator/facilitator-reg-daemon @@ -8,7 +8,9 @@ import sys import threading import time -import fac +from flashproxy import fac +from flashproxy import proc +from flashproxy.util import format_addr from M2Crypto import RSA @@ -98,7 +100,7 @@ class Handler(SocketServer.StreamRequestHandler): raise socket.error("refusing to buffer %d bytes (last read was %d bytes)" % (buflen, len(data))) return self.buffer - @fac.catch_epipe + @proc.catch_epipe def handle(self): try: b64_ciphertext = self.read_input() @@ -109,7 +111,7 @@ class Handler(SocketServer.StreamRequestHandler): ciphertext = b64_ciphertext.decode("base64") plaintext = rsa.private_decrypt(ciphertext, RSA.pkcs1_oaep_padding) for client_reg in fac.read_client_registrations(plaintext): - log(u"registering %s" % safe_str(fac.format_addr(client_reg.addr))) + log(u"registering %s" % safe_str(format_addr(client_reg.addr))) if not fac.put_reg(FACILITATOR_ADDR, client_reg.addr, client_reg.transport): print >> self.wfile, "FAIL" break @@ -120,7 +122,7 @@ class Handler(SocketServer.StreamRequestHandler): print >> self.wfile, "FAIL" raise - finish = fac.catch_epipe(SocketServer.StreamRequestHandler.finish) + finish = proc.catch_epipe(SocketServer.StreamRequestHandler.finish) class Server(SocketServer.ThreadingMixIn, SocketServer.TCPServer): allow_reuse_address = True @@ -164,7 +166,7 @@ def main(): print >> sys.stderr, "Failed to open private key file \"%s\": %s." % (options.key_filename, str(e)) sys.exit(1) try: - if not fac.check_perms(key_file.fileno()): + if not proc.check_perms(key_file.fileno()): print >> sys.stderr, "Refusing to run with group- or world-readable private key file. Try" print >> sys.stderr, "\tchmod 600 %s" % options.key_filename sys.exit(1) @@ -183,7 +185,7 @@ def main(): server = Server(addrinfo[4], Handler) - log(u"start on %s" % fac.format_addr(addrinfo[4])) + log(u"start on %s" % format_addr(addrinfo[4])) if options.daemonize: log(u"daemonizing") @@ -198,7 +200,7 @@ def main(): if options.privdrop_username is not None: log(u"dropping privileges to those of user %s" % options.privdrop_username) try: - fac.drop_privs(options.privdrop_username) + proc.drop_privs(options.privdrop_username) except BaseException, e: print >> sys.stderr, "Can't drop privileges:", str(e) sys.exit(1) diff --git a/facilitator/facilitator-test.py b/facilitator/facilitator-test.py index d63e0b5..fd4ac88 100755 --- a/facilitator/facilitator-test.py +++ b/facilitator/facilitator-test.py @@ -9,8 +9,9 @@ import sys import time import unittest -import fac -from fac import Transport, Endpoint +from flashproxy import fac +from flashproxy.reg import Transport, Endpoint +from flashproxy.util import format_addr # Import the facilitator program as a module. import imp @@ -178,19 +179,6 @@ class EndpointsTest(unittest.TestCase): class FacilitatorTest(unittest.TestCase): - def test_transport_parse(self): - self.assertEquals(Transport.parse("a"), Transport("", "a")) - self.assertEquals(Transport.parse("|a"), Transport("", "a")) - self.assertEquals(Transport.parse("a|b|c"), Transport("a|b","c")) - self.assertEquals(Transport.parse(Transport("a|b","c")), Transport("a|b","c")) - self.assertRaises(ValueError, Transport, "", "") - self.assertRaises(ValueError, Transport, "a", "") - self.assertRaises(ValueError, Transport.parse, "") - self.assertRaises(ValueError, Transport.parse, "|") - self.assertRaises(ValueError, Transport.parse, "a|") - self.assertRaises(ValueError, Transport.parse, ["a"]) - self.assertRaises(ValueError, Transport.parse, [Transport("a", "b")]) - def test_parse_relay_file(self): fp = StringIO() fp.write("websocket 0.0.1.0:1\n") @@ -201,6 +189,7 @@ class FacilitatorTest(unittest.TestCase): parse_relay_file(servers, fp) self.assertEquals(servers[af]._endpoints, {('0.0.1.0', 1): Transport('', 'websocket')}) + class FacilitatorProcTest(unittest.TestCase): IPV4_CLIENT_ADDR = ("1.1.1.1", 9000) IPV6_CLIENT_ADDR = ("[11::11]", 9000) @@ -214,8 +203,8 @@ class FacilitatorProcTest(unittest.TestCase): def setUp(self): self.relay_file = tempfile.NamedTemporaryFile() - self.relay_file.write("%s %s\n" % (RELAY_TP, fac.format_addr(self.IPV4_RELAY_ADDR))) - self.relay_file.write("%s %s\n" % (RELAY_TP, fac.format_addr(self.IPV6_RELAY_ADDR))) + self.relay_file.write("%s %s\n" % (RELAY_TP, format_addr(self.IPV4_RELAY_ADDR))) + self.relay_file.write("%s %s\n" % (RELAY_TP, format_addr(self.IPV6_RELAY_ADDR))) self.relay_file.flush() self.relay_file.seek(0) fn = os.path.join(os.path.dirname(__file__), "./facilitator") @@ -261,7 +250,7 @@ class FacilitatorProcTest(unittest.TestCase): fac.put_reg(FACILITATOR_ADDR, self.IPV4_CLIENT_ADDR, CLIENT_TP) fac.put_reg(FACILITATOR_ADDR, self.IPV6_CLIENT_ADDR, CLIENT_TP) reg = fac.get_reg(FACILITATOR_ADDR, self.IPV4_PROXY_ADDR, PROXY_TPS) - self.assertEqual(reg["client"], fac.format_addr(self.IPV4_CLIENT_ADDR)) + self.assertEqual(reg["client"], format_addr(self.IPV4_CLIENT_ADDR)) def test_af_v4_v6(self): """Test that IPv4 proxies do not get IPv6 clients.""" @@ -280,15 +269,15 @@ class FacilitatorProcTest(unittest.TestCase): fac.put_reg(FACILITATOR_ADDR, self.IPV4_CLIENT_ADDR, CLIENT_TP) fac.put_reg(FACILITATOR_ADDR, self.IPV6_CLIENT_ADDR, CLIENT_TP) reg = fac.get_reg(FACILITATOR_ADDR, self.IPV6_PROXY_ADDR, PROXY_TPS) - self.assertEqual(reg["client"], fac.format_addr(self.IPV6_CLIENT_ADDR)) + self.assertEqual(reg["client"], format_addr(self.IPV6_CLIENT_ADDR)) def test_fields(self): """Test that facilitator responses contain all the required fields.""" fac.put_reg(FACILITATOR_ADDR, self.IPV4_CLIENT_ADDR, CLIENT_TP) reg = fac.get_reg(FACILITATOR_ADDR, self.IPV4_PROXY_ADDR, PROXY_TPS) - self.assertEqual(reg["client"], fac.format_addr(self.IPV4_CLIENT_ADDR)) + self.assertEqual(reg["client"], format_addr(self.IPV4_CLIENT_ADDR)) self.assertEqual(reg["client-transport"], CLIENT_TP) - self.assertEqual(reg["relay"], fac.format_addr(self.IPV4_RELAY_ADDR)) + self.assertEqual(reg["relay"], format_addr(self.IPV4_RELAY_ADDR)) self.assertEqual(reg["relay-transport"], RELAY_TP) self.assertGreater(int(reg["check-back-in"]), 0) @@ -323,117 +312,5 @@ class FacilitatorProcTest(unittest.TestCase): # """Test that the facilitator rejects hostnames.""" # self.fail() -class ParseAddrSpecTest(unittest.TestCase): - def test_ipv4(self): - self.assertEqual(fac.parse_addr_spec("192.168.0.1:9999"), ("192.168.0.1", 9999)) - - def test_ipv6(self): - self.assertEqual(fac.parse_addr_spec("[12::34]:9999"), ("12::34", 9999)) - - def test_defhost_defport_ipv4(self): - self.assertEqual(fac.parse_addr_spec("192.168.0.2:8888", defhost="192.168.0.1", defport=9999), ("192.168.0.2", 8888)) - self.assertEqual(fac.parse_addr_spec("192.168.0.2:", defhost="192.168.0.1", defport=9999), ("192.168.0.2", 9999)) - self.assertEqual(fac.parse_addr_spec("192.168.0.2", defhost="192.168.0.1", defport=9999), ("192.168.0.2", 9999)) - self.assertEqual(fac.parse_addr_spec(":8888", defhost="192.168.0.1", defport=9999), ("192.168.0.1", 8888)) - self.assertEqual(fac.parse_addr_spec(":", defhost="192.168.0.1", defport=9999), ("192.168.0.1", 9999)) - self.assertEqual(fac.parse_addr_spec("", defhost="192.168.0.1", defport=9999), ("192.168.0.1", 9999)) - - def test_defhost_defport_ipv6(self): - self.assertEqual(fac.parse_addr_spec("[1234::2]:8888", defhost="1234::1", defport=9999), ("1234::2", 8888)) - self.assertEqual(fac.parse_addr_spec("[1234::2]:", defhost="1234::1", defport=9999), ("1234::2", 9999)) - self.assertEqual(fac.parse_addr_spec("[1234::2]", defhost="1234::1", defport=9999), ("1234::2", 9999)) - self.assertEqual(fac.parse_addr_spec(":8888", defhost="1234::1", defport=9999), ("1234::1", 8888)) - self.assertEqual(fac.parse_addr_spec(":", defhost="1234::1", defport=9999), ("1234::1", 9999)) - self.assertEqual(fac.parse_addr_spec("", defhost="1234::1", defport=9999), ("1234::1", 9999)) - - def test_canonical_ip_noresolve(self): - """Test that canonical_ip does not do DNS resolution by default.""" - self.assertRaises(ValueError, fac.canonical_ip, *fac.parse_addr_spec("example.com:80")) - -class ParseTransactionTest(unittest.TestCase): - def test_empty_string(self): - self.assertRaises(ValueError, fac.parse_transaction, "") - - def test_correct(self): - self.assertEqual(fac.parse_transaction("COMMAND"), ("COMMAND", ())) - self.assertEqual(fac.parse_transaction("COMMAND X=\"\""), ("COMMAND", (("X", ""),))) - self.assertEqual(fac.parse_transaction("COMMAND X=\"ABC\""), ("COMMAND", (("X", "ABC"),))) - self.assertEqual(fac.parse_transaction("COMMAND X=\"\\A\\B\\C\""), ("COMMAND", (("X", "ABC"),))) - self.assertEqual(fac.parse_transaction("COMMAND X=\"\\\\\\\"\""), ("COMMAND", (("X", "\\\""),))) - self.assertEqual(fac.parse_transaction("COMMAND X=\"ABC\" Y=\"DEF\""), ("COMMAND", (("X", "ABC"), ("Y", "DEF")))) - self.assertEqual(fac.parse_transaction("COMMAND KEY-NAME=\"ABC\""), ("COMMAND", (("KEY-NAME", "ABC"),))) - self.assertEqual(fac.parse_transaction("COMMAND KEY_NAME=\"ABC\""), ("COMMAND", (("KEY_NAME", "ABC"),))) - - def test_missing_command(self): - self.assertRaises(ValueError, fac.parse_transaction, "X=\"ABC\"") - self.assertRaises(ValueError, fac.parse_transaction, " X=\"ABC\"") - - def test_missing_space(self): - self.assertRaises(ValueError, fac.parse_transaction, "COMMAND/X=\"ABC\"") - self.assertRaises(ValueError, fac.parse_transaction, "COMMAND X=\"ABC\"Y=\"DEF\"") - - def test_bad_quotes(self): - self.assertRaises(ValueError, fac.parse_transaction, "COMMAND X=\"") - self.assertRaises(ValueError, fac.parse_transaction, "COMMAND X=\"ABC") - self.assertRaises(ValueError, fac.parse_transaction, "COMMAND X=\"ABC\" Y=\"ABC") - self.assertRaises(ValueError, fac.parse_transaction, "COMMAND X=\"ABC\\") - - def test_truncated(self): - self.assertRaises(ValueError, fac.parse_transaction, "COMMAND X=") - - def test_newline(self): - self.assertRaises(ValueError, fac.parse_transaction, "COMMAND X=\"ABC\" \nY=\"DEF\"") - -class ReadClientRegistrationsTest(unittest.TestCase): - def testSingle(self): - l = list(fac.read_client_registrations("")) - self.assertEqual(len(l), 0) - l = list(fac.read_client_registrations("client=1.2.3.4:1111")) - self.assertEqual(len(l), 1) - self.assertEqual(l[0].addr, ("1.2.3.4", 1111)) - l = list(fac.read_client_registrations("client=1.2.3.4:1111\n")) - self.assertEqual(len(l), 1) - self.assertEqual(l[0].addr, ("1.2.3.4", 1111)) - l = list(fac.read_client_registrations("foo=bar&client=1.2.3.4:1111&baz=quux")) - self.assertEqual(len(l), 1) - self.assertEqual(l[0].addr, ("1.2.3.4", 1111)) - l = list(fac.read_client_registrations("foo=b%3dar&client=1.2.3.4%3a1111")) - self.assertEqual(len(l), 1) - self.assertEqual(l[0].addr, ("1.2.3.4", 1111)) - l = list(fac.read_client_registrations("client=%5b1::2%5d:3333")) - self.assertEqual(len(l), 1) - self.assertEqual(l[0].addr, ("1::2", 3333)) - - def testDefaultAddress(self): - l = list(fac.read_client_registrations("client=:1111&transport=websocket", defhost="1.2.3.4")) - self.assertEqual(l[0].addr, ("1.2.3.4", 1111)) - l = list(fac.read_client_registrations("client=1.2.3.4:&transport=websocket", defport=1111)) - self.assertEqual(l[0].addr, ("1.2.3.4", 1111)) - - def testDefaultTransport(self): - l = list(fac.read_client_registrations("client=1.2.3.4:1111")) - self.assertEqual(l[0].transport, "websocket") - - def testMultiple(self): - l = list(fac.read_client_registrations("client=1.2.3.4:1111&foo=bar\nfoo=bar&client=5.6.7.8:2222")) - self.assertEqual(len(l), 2) - self.assertEqual(l[0].addr, ("1.2.3.4", 1111)) - self.assertEqual(l[1].addr, ("5.6.7.8", 2222)) - l = list(fac.read_client_registrations("client=1.2.3.4:1111&foo=bar\nfoo=bar&client=%5b1::2%5d:3333")) - self.assertEqual(len(l), 2) - self.assertEqual(l[0].addr, ("1.2.3.4", 1111)) - self.assertEqual(l[1].addr, ("1::2", 3333)) - - def testInvalid(self): - # Missing "client". - with self.assertRaises(ValueError): - list(fac.read_client_registrations("foo=bar")) - # More than one "client". - with self.assertRaises(ValueError): - list(fac.read_client_registrations("client=1.2.3.4:1111&foo=bar&client=5.6.7.8:2222")) - # Single client with bad syntax. - with self.assertRaises(ValueError): - list(fac.read_client_registrations("client=1.2.3.4,1111")) - if __name__ == "__main__": unittest.main() diff --git a/facilitator/facilitator.cgi b/facilitator/facilitator.cgi index 42a888e..addcaf4 100755 --- a/facilitator/facilitator.cgi +++ b/facilitator/facilitator.cgi @@ -6,7 +6,7 @@ import socket import sys import urllib -import fac +from flashproxy import fac FACILITATOR_ADDR = ("127.0.0.1", 9002) diff --git a/flashproxy-client b/flashproxy-client index e54e554..f2cf04b 100755 --- a/flashproxy-client +++ b/flashproxy-client @@ -1129,14 +1129,14 @@ def main(): default_remote_port = DEFAULT_REMOTE_PORT if len(args) == 0: - local_addr = (None, default_local_port) - remote_addr = (None, default_remote_port) + local_addr = ("", default_local_port) + remote_addr = ("", default_remote_port) elif len(args) == 1: - local_addr = parse_addr_spec(args[0], defport=default_local_port) - remote_addr = (None, default_remote_port) + local_addr = parse_addr_spec(args[0], defhost="", defport=default_local_port) + remote_addr = ("", default_remote_port) elif len(args) == 2: - local_addr = parse_addr_spec(args[0], defport=default_local_port) - remote_addr = parse_addr_spec(args[1], defport=default_remote_port) + local_addr = parse_addr_spec(args[0], defhost="", defport=default_local_port) + remote_addr = parse_addr_spec(args[1], defhost="", defport=default_remote_port) else: usage(sys.stderr) sys.exit(1) diff --git a/flashproxy-reg-appspot b/flashproxy-reg-appspot index 516fcc9..7738e43 100755 --- a/flashproxy-reg-appspot +++ b/flashproxy-reg-appspot @@ -19,7 +19,7 @@ except ImportError: # Defer the error reporting so that --help works even without M2Crypto. SSL = None -DEFAULT_REMOTE_ADDRESS = None +DEFAULT_REMOTE_ADDRESS = "" DEFAULT_REMOTE_PORT = 9000 DEFAULT_TRANSPORT = "websocket" diff --git a/flashproxy-reg-email b/flashproxy-reg-email index ab8d4cb..d286af8 100755 --- a/flashproxy-reg-email +++ b/flashproxy-reg-email @@ -20,7 +20,7 @@ except ImportError: RSA = None SSL = None -DEFAULT_REMOTE_ADDRESS = None +DEFAULT_REMOTE_ADDRESS = "" DEFAULT_REMOTE_PORT = 9000 DEFAULT_EMAIL_ADDRESS = "flashproxyreg.a@gmail.com" # dig MX gmail.com diff --git a/flashproxy-reg-url b/flashproxy-reg-url index c45bc26..0f92d44 100755 --- a/flashproxy-reg-url +++ b/flashproxy-reg-url @@ -15,7 +15,7 @@ except ImportError: # Defer the error reporting so that --help works even without M2Crypto. RSA = None -DEFAULT_REMOTE_ADDRESS = None +DEFAULT_REMOTE_ADDRESS = "" DEFAULT_REMOTE_PORT = 9000 DEFAULT_FACILITATOR_URL = "https://fp-facilitator.org/" DEFAULT_TRANSPORT = "websocket" diff --git a/flashproxy/fac.py b/flashproxy/fac.py new file mode 100644 index 0000000..0686f54 --- /dev/null +++ b/flashproxy/fac.py @@ -0,0 +1,224 @@ +import socket +import subprocess +import urlparse + +from flashproxy import reg +from flashproxy.util import parse_addr_spec, format_addr + +DEFAULT_CLIENT_TRANSPORT = "websocket" + +def read_client_registrations(body, defhost=None, defport=None): + """Yield client registrations (as Endpoints) from an encoded registration + message body. The message format is one registration per line, with each + line being encoded as application/x-www-form-urlencoded. The key "client" is + required and contains the client address and port (perhaps filled in by + defhost and defport). The key "client-transport" is optional and defaults to + "websocket". + Example: + client=1.2.3.4:9000&client-transport=websocket + client=1.2.3.4:9090&client-transport=obfs3|websocket + """ + for line in body.splitlines(): + qs = urlparse.parse_qs(line, keep_blank_values=True, strict_parsing=True) + # Get the unique value associated with the given key in qs. If the key + # is absent or appears more than once, raise ValueError. + def get_unique(key, default=None): + try: + vals = qs[key] + except KeyError: + if default is None: + raise ValueError("missing %r key" % key) + vals = (default,) + if len(vals) != 1: + raise ValueError("more than one %r key" % key) + return vals[0] + addr = parse_addr_spec(get_unique("client"), defhost, defport) + transport = get_unique("client-transport", DEFAULT_CLIENT_TRANSPORT) + yield reg.Endpoint(addr, transport) + +def skip_space(pos, line): + """Skip a (possibly empty) sequence of space characters (the ASCII character + '\x20' exactly). Returns a pair (pos, num_skipped).""" + begin = pos + while pos < len(line) and line[pos] == "\x20": + pos += 1 + return pos, pos - begin + +TOKEN_CHARS = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-") +def get_token(pos, line): + begin = pos + while pos < len(line) and line[pos] in TOKEN_CHARS: + pos += 1 + if begin == pos: + raise ValueError("No token found at position %d" % pos) + return pos, line[begin:pos] + +def get_quoted_string(pos, line): + chars = [] + if not (pos < len(line) and line[pos] == '"'): + raise ValueError("Expected '\"' at beginning of quoted string.") + pos += 1 + while pos < len(line) and line[pos] != '"': + if line[pos] == '\\': + pos += 1 + if not (pos < len(line)): + raise ValueError("End of line after backslash in quoted string") + chars.append(line[pos]) + pos += 1 + if not (pos < len(line) and line[pos] == '"'): + raise ValueError("Expected '\"' at end of quoted string.") + pos += 1 + return pos, "".join(chars) + +def parse_transaction(line): + """A transaction is a command followed by zero or more key-value pairs. Like so: + COMMAND KEY="VALUE" KEY="\"ESCAPED\" VALUE" + Values must be quoted. Any byte value may be escaped with a backslash. + Returns a pair: (COMMAND, ((KEY1, VALUE1), (KEY2, VALUE2), ...)). + """ + pos = 0 + pos, skipped = skip_space(pos, line) + pos, command = get_token(pos, line) + + pairs = [] + while True: + pos, skipped = skip_space(pos, line) + if not (pos < len(line)): + break + if skipped == 0: + raise ValueError("Expected space before key-value pair") + pos, key = get_token(pos, line) + if not (pos < len(line) and line[pos] == '='): + raise ValueError("No '=' found after key") + pos += 1 + pos, value = get_quoted_string(pos, line) + pairs.append((key, value)) + return command, tuple(pairs) + +def param_first(key, params): + """Search 'params' for 'key' and return the first value that + occurs. If 'key' was not found, return None.""" + for k, v in params: + if key == k: + return v + return None + +def param_getlist(key, params): + """Search 'params' for 'key' and return a list with its values. If + 'key' did not appear in 'params', return the empty list.""" + result = [] + for k, v in params: + if key == k: + result.append(v) + return result + +def quote_string(s): + chars = [] + for c in s: + if c == "\\": + c = "\\\\" + elif c == "\"": + c = "\\\"" + chars.append(c) + return "\"" + "".join(chars) + "\"" + +def render_transaction(command, *params): + parts = [command] + for key, value in params: + parts.append("%s=%s" % (key, quote_string(value))) + return " ".join(parts) + +def fac_socket(facilitator_addr): + return socket.create_connection(facilitator_addr, 1.0).makefile() + +def transact(f, command, *params): + transaction = render_transaction(command, *params) + print >> f, transaction + f.flush() + line = f.readline() + if not (len(line) > 0 and line[-1] == '\n'): + raise ValueError("No newline at end of string returned by facilitator") + return parse_transaction(line[:-1]) + +def put_reg(facilitator_addr, client_addr, transport): + """Send a registration to the facilitator using a one-time socket. Returns + true iff the command was successful. transport is a transport string such as + "websocket" or "obfs3|websocket".""" + f = fac_socket(facilitator_addr) + params = [("CLIENT", format_addr(client_addr))] + params.append(("TRANSPORT", transport)) + try: + command, params = transact(f, "PUT", *params) + finally: + f.close() + return command == "OK" + +def get_reg(facilitator_addr, proxy_addr, proxy_transport_list): + """ + Get a client registration for proxy proxy_addr from the + facilitator at facilitator_addr using a one-time + socket. proxy_transport_list is a list containing the transport names that + the flashproxy supports. + + Returns a dict with keys "client", "client-transport", "relay", + and "relay-transport" if successful, or a dict with the key "client" + mapped to the value "" if there are no registrations available for + proxy_addr. Raises an exception otherwise.""" + f = fac_socket(facilitator_addr) + + # Form a list (in transact() format) with the transports that we + # should send to the facilitator. Then pass that list to the + # transact() function. + # For example, PROXY-TRANSPORT=obfs2 PROXY-TRANSPORT=obfs3. + transports = [("PROXY-TRANSPORT", tp) for tp in proxy_transport_list] + + try: + command, params = transact(f, "GET", ("FROM", format_addr(proxy_addr)), *transports) + finally: + f.close() + response = {} + check_back_in = param_first("CHECK-BACK-IN", params) + if check_back_in is not None: + try: + float(check_back_in) + except ValueError: + raise ValueError("Facilitator returned non-numeric polling interval.") + response["check-back-in"] = check_back_in + if command == "NONE": + response["client"] = "" + return response + elif command == "OK": + client_spec = param_first("CLIENT", params) + client_transport = param_first("CLIENT-TRANSPORT", params) + relay_spec = param_first("RELAY", params) + relay_transport = param_first("RELAY-TRANSPORT", params) + if not client_spec: + raise ValueError("Facilitator did not return CLIENT") + if not client_transport: + raise ValueError("Facilitator did not return CLIENT-TRANSPORT") + if not relay_spec: + raise ValueError("Facilitator did not return RELAY") + if not relay_transport: + raise ValueError("Facilitator did not return RELAY-TRANSPORT") + # Check the syntax returned by the facilitator. + client = parse_addr_spec(client_spec) + relay = parse_addr_spec(relay_spec) + response["client"] = format_addr(client) + response["client-transport"] = client_transport + response["relay"] = format_addr(relay) + response["relay-transport"] = relay_transport + return response + else: + raise ValueError("Facilitator response was not \"OK\"") + +def put_reg_base64(b64): + """Attempt to add a registration by running a facilitator-reg program + locally.""" + # Padding is optional, but the python base64 functions can't + # handle lack of padding. Add it here. Assumes correct encoding. + mod = len(b64) % 4 + if mod != 0: + b64 += (4 - mod) * "=" + p = subprocess.Popen(["facilitator-reg"], stdin=subprocess.PIPE) + stdout, stderr = p.communicate(b64) + return p.returncode == 0 diff --git a/flashproxy/proc.py b/flashproxy/proc.py new file mode 100644 index 0000000..4a008b2 --- /dev/null +++ b/flashproxy/proc.py @@ -0,0 +1,47 @@ +import errno +import os +import socket +import stat +import pwd + +DEFAULT_CLIENT_TRANSPORT = "websocket" + +# Return true iff the given fd is readable, writable, and executable only by its +# owner. +def check_perms(fd): + mode = os.fstat(fd)[0] + return (mode & (stat.S_IRWXG | stat.S_IRWXO)) == 0 + +# Drop privileges by switching ID to that of the given user. +# http://stackoverflow.com/questions/2699907/dropping-root-permissions-in-pyth... +# https://www.securecoding.cert.org/confluence/display/seccode/POS36-C.+Observ... +# https://www.securecoding.cert.org/confluence/display/seccode/POS37-C.+Ensure... +def drop_privs(username): + uid = pwd.getpwnam(username).pw_uid + gid = pwd.getpwnam(username).pw_gid + os.setgroups([]) + os.setgid(gid) + os.setuid(uid) + try: + os.setuid(0) + except OSError: + pass + else: + raise AssertionError("setuid(0) succeeded after attempting to drop privileges") + +# A decorator to ignore "broken pipe" errors. +def catch_epipe(fn): + def ret(self, *args): + try: + return fn(self, *args) + except socket.error, e: + try: + err_num = e.errno + except AttributeError: + # Before Python 2.6, exception can be a pair. + err_num, errstr = e + except: + raise + if err_num != errno.EPIPE: + raise + return ret diff --git a/flashproxy/reg.py b/flashproxy/reg.py new file mode 100644 index 0000000..0551f06 --- /dev/null +++ b/flashproxy/reg.py @@ -0,0 +1,31 @@ +from collections import namedtuple + +from flashproxy.util import parse_addr_spec + +class Transport(namedtuple("Transport", "inner outer")): + @classmethod + def parse(cls, transport): + if isinstance(transport, cls): + return transport + elif type(transport) == str: + if "|" in transport: + inner, outer = transport.rsplit("|", 1) + else: + inner, outer = "", transport + return cls(inner, outer) + else: + raise ValueError("could not parse transport: %s" % transport) + + def __init__(self, inner, outer): + if not outer: + raise ValueError("outer (proxy) part of transport must be non-empty: %s" % str(self)) + + def __str__(self): + return "%s|%s" % (self.inner, self.outer) if self.inner else self.outer + + +class Endpoint(namedtuple("Endpoint", "addr transport")): + @classmethod + def parse(cls, spec, transport, defhost = None, defport = None): + host, port = parse_addr_spec(spec, defhost, defport) + return cls((host, port), Transport.parse(transport)) diff --git a/flashproxy/test/test_fac.py b/flashproxy/test/test_fac.py new file mode 100644 index 0000000..e7dfa00 --- /dev/null +++ b/flashproxy/test/test_fac.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python + +import unittest + +from flashproxy.fac import parse_transaction, read_client_registrations + +class ParseTransactionTest(unittest.TestCase): + def test_empty_string(self): + self.assertRaises(ValueError, parse_transaction, "") + + def test_correct(self): + self.assertEqual(parse_transaction("COMMAND"), ("COMMAND", ())) + self.assertEqual(parse_transaction("COMMAND X=\"\""), ("COMMAND", (("X", ""),))) + self.assertEqual(parse_transaction("COMMAND X=\"ABC\""), ("COMMAND", (("X", "ABC"),))) + self.assertEqual(parse_transaction("COMMAND X=\"\\A\\B\\C\""), ("COMMAND", (("X", "ABC"),))) + self.assertEqual(parse_transaction("COMMAND X=\"\\\\\\\"\""), ("COMMAND", (("X", "\\\""),))) + self.assertEqual(parse_transaction("COMMAND X=\"ABC\" Y=\"DEF\""), ("COMMAND", (("X", "ABC"), ("Y", "DEF")))) + self.assertEqual(parse_transaction("COMMAND KEY-NAME=\"ABC\""), ("COMMAND", (("KEY-NAME", "ABC"),))) + self.assertEqual(parse_transaction("COMMAND KEY_NAME=\"ABC\""), ("COMMAND", (("KEY_NAME", "ABC"),))) + + def test_missing_command(self): + self.assertRaises(ValueError, parse_transaction, "X=\"ABC\"") + self.assertRaises(ValueError, parse_transaction, " X=\"ABC\"") + + def test_missing_space(self): + self.assertRaises(ValueError, parse_transaction, "COMMAND/X=\"ABC\"") + self.assertRaises(ValueError, parse_transaction, "COMMAND X=\"ABC\"Y=\"DEF\"") + + def test_bad_quotes(self): + self.assertRaises(ValueError, parse_transaction, "COMMAND X=\"") + self.assertRaises(ValueError, parse_transaction, "COMMAND X=\"ABC") + self.assertRaises(ValueError, parse_transaction, "COMMAND X=\"ABC\" Y=\"ABC") + self.assertRaises(ValueError, parse_transaction, "COMMAND X=\"ABC\\") + + def test_truncated(self): + self.assertRaises(ValueError, parse_transaction, "COMMAND X=") + + def test_newline(self): + self.assertRaises(ValueError, parse_transaction, "COMMAND X=\"ABC\" \nY=\"DEF\"") + +class ReadClientRegistrationsTest(unittest.TestCase): + def testSingle(self): + l = list(read_client_registrations("")) + self.assertEqual(len(l), 0) + l = list(read_client_registrations("client=1.2.3.4:1111")) + self.assertEqual(len(l), 1) + self.assertEqual(l[0].addr, ("1.2.3.4", 1111)) + l = list(read_client_registrations("client=1.2.3.4:1111\n")) + self.assertEqual(len(l), 1) + self.assertEqual(l[0].addr, ("1.2.3.4", 1111)) + l = list(read_client_registrations("foo=bar&client=1.2.3.4:1111&baz=quux")) + self.assertEqual(len(l), 1) + self.assertEqual(l[0].addr, ("1.2.3.4", 1111)) + l = list(read_client_registrations("foo=b%3dar&client=1.2.3.4%3a1111")) + self.assertEqual(len(l), 1) + self.assertEqual(l[0].addr, ("1.2.3.4", 1111)) + l = list(read_client_registrations("client=%5b1::2%5d:3333")) + self.assertEqual(len(l), 1) + self.assertEqual(l[0].addr, ("1::2", 3333)) + + def testDefaultAddress(self): + l = list(read_client_registrations("client=:1111&transport=websocket", defhost="1.2.3.4")) + self.assertEqual(l[0].addr, ("1.2.3.4", 1111)) + l = list(read_client_registrations("client=1.2.3.4:&transport=websocket", defport=1111)) + self.assertEqual(l[0].addr, ("1.2.3.4", 1111)) + + def testDefaultTransport(self): + l = list(read_client_registrations("client=1.2.3.4:1111")) + self.assertEqual(l[0].transport, "websocket") + + def testMultiple(self): + l = list(read_client_registrations("client=1.2.3.4:1111&foo=bar\nfoo=bar&client=5.6.7.8:2222")) + self.assertEqual(len(l), 2) + self.assertEqual(l[0].addr, ("1.2.3.4", 1111)) + self.assertEqual(l[1].addr, ("5.6.7.8", 2222)) + l = list(read_client_registrations("client=1.2.3.4:1111&foo=bar\nfoo=bar&client=%5b1::2%5d:3333")) + self.assertEqual(len(l), 2) + self.assertEqual(l[0].addr, ("1.2.3.4", 1111)) + self.assertEqual(l[1].addr, ("1::2", 3333)) + + def testInvalid(self): + # Missing "client". + with self.assertRaises(ValueError): + list(read_client_registrations("foo=bar")) + # More than one "client". + with self.assertRaises(ValueError): + list(read_client_registrations("client=1.2.3.4:1111&foo=bar&client=5.6.7.8:2222")) + # Single client with bad syntax. + with self.assertRaises(ValueError): + list(read_client_registrations("client=1.2.3.4,1111")) + +if __name__ == "__main__": + unittest.main() diff --git a/flashproxy/test/test_reg.py b/flashproxy/test/test_reg.py new file mode 100644 index 0000000..6b0e196 --- /dev/null +++ b/flashproxy/test/test_reg.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python + +import unittest + +from flashproxy.reg import Transport + +class TransportTest(unittest.TestCase): + + def test_transport_parse(self): + self.assertEquals(Transport.parse("a"), Transport("", "a")) + self.assertEquals(Transport.parse("|a"), Transport("", "a")) + self.assertEquals(Transport.parse("a|b|c"), Transport("a|b","c")) + self.assertEquals(Transport.parse(Transport("a|b","c")), Transport("a|b","c")) + self.assertRaises(ValueError, Transport, "", "") + self.assertRaises(ValueError, Transport, "a", "") + self.assertRaises(ValueError, Transport.parse, "") + self.assertRaises(ValueError, Transport.parse, "|") + self.assertRaises(ValueError, Transport.parse, "a|") + self.assertRaises(ValueError, Transport.parse, ["a"]) + self.assertRaises(ValueError, Transport.parse, [Transport("a", "b")]) + +if __name__ == "__main__": + unittest.main() diff --git a/flashproxy/test/test_util.py b/flashproxy/test/test_util.py new file mode 100644 index 0000000..af4c2e6 --- /dev/null +++ b/flashproxy/test/test_util.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python + +import unittest + +from flashproxy.util import parse_addr_spec, canonical_ip + +class ParseAddrSpecTest(unittest.TestCase): + def test_ipv4(self): + self.assertEqual(parse_addr_spec("192.168.0.1:9999"), ("192.168.0.1", 9999)) + + def test_ipv6(self): + self.assertEqual(parse_addr_spec("[12::34]:9999"), ("12::34", 9999)) + + def test_defhost_defport_ipv4(self): + self.assertEqual(parse_addr_spec("192.168.0.2:8888", defhost="192.168.0.1", defport=9999), ("192.168.0.2", 8888)) + self.assertEqual(parse_addr_spec("192.168.0.2:", defhost="192.168.0.1", defport=9999), ("192.168.0.2", 9999)) + self.assertEqual(parse_addr_spec("192.168.0.2", defhost="192.168.0.1", defport=9999), ("192.168.0.2", 9999)) + self.assertEqual(parse_addr_spec(":8888", defhost="192.168.0.1", defport=9999), ("192.168.0.1", 8888)) + self.assertEqual(parse_addr_spec(":", defhost="192.168.0.1", defport=9999), ("192.168.0.1", 9999)) + self.assertEqual(parse_addr_spec("", defhost="192.168.0.1", defport=9999), ("192.168.0.1", 9999)) + + def test_defhost_defport_ipv6(self): + self.assertEqual(parse_addr_spec("[1234::2]:8888", defhost="1234::1", defport=9999), ("1234::2", 8888)) + self.assertEqual(parse_addr_spec("[1234::2]:", defhost="1234::1", defport=9999), ("1234::2", 9999)) + self.assertEqual(parse_addr_spec("[1234::2]", defhost="1234::1", defport=9999), ("1234::2", 9999)) + self.assertEqual(parse_addr_spec(":8888", defhost="1234::1", defport=9999), ("1234::1", 8888)) + self.assertEqual(parse_addr_spec(":", defhost="1234::1", defport=9999), ("1234::1", 9999)) + self.assertEqual(parse_addr_spec("", defhost="1234::1", defport=9999), ("1234::1", 9999)) + + def test_canonical_ip_noresolve(self): + """Test that canonical_ip does not do DNS resolution by default.""" + self.assertRaises(ValueError, canonical_ip, *parse_addr_spec("example.com:80")) + +if __name__ == "__main__": + unittest.main() diff --git a/flashproxy/util.py b/flashproxy/util.py index 47bd87a..b069bf7 100644 --- a/flashproxy/util.py +++ b/flashproxy/util.py @@ -2,6 +2,27 @@ import re import socket def parse_addr_spec(spec, defhost = None, defport = None): + """Parse a host:port specification and return a 2-tuple ("host", port) as + understood by the Python socket functions. + >>> parse_addr_spec("192.168.0.1:9999") + ('192.168.0.1', 9999) + + If defhost or defport are given, those parts of the specification may be + omitted; if so, they will be filled in with defaults. + >>> parse_addr_spec("192.168.0.2:8888", defhost="192.168.0.1", defport=9999) + ('192.168.0.2', 8888) + >>> parse_addr_spec(":8888", defhost="192.168.0.1", defport=9999) + ('192.168.0.1', 8888) + >>> parse_addr_spec("192.168.0.2", defhost="192.168.0.1", defport=9999) + ('192.168.0.2', 9999) + >>> parse_addr_spec("192.168.0.2:", defhost="192.168.0.1", defport=9999) + ('192.168.0.2', 9999) + >>> parse_addr_spec(":", defhost="192.168.0.1", defport=9999) + ('192.168.0.1', 9999) + >>> parse_addr_spec("", defhost="192.168.0.1", defport=9999) + ('192.168.0.1', 9999) + + IPv6 addresses must be enclosed in square brackets.""" host = None port = None af = 0 @@ -29,24 +50,61 @@ def parse_addr_spec(spec, defhost = None, defport = None): af = 0 host = host or defhost port = port or defport - if port is not None: - port = int(port) - return host, port + if host is None or port is None: + raise ValueError("Bad address specification \"%s\"" % spec) + return host, int(port) -def format_addr(addr): - host, port = addr - if not host: - return u":%d" % port - # Numeric IPv6 address? +def resolve_to_ip(host, port, af=0, gai_flags=0): + """Resolves a host string to an IP address in canonical format. + + Note: in many cases this is not necessary since the consumer of the address + can probably accept host names directly. + + :param: host string to resolve; may be a DNS name or an IP address. + :param: port of the host + :param: af address family, default unspecified. set to socket.AF_INET or + socket.AF_INET6 to force IPv4 or IPv6 name resolution. + :returns: (IP address in canonical format, port) + """ + # Forward-resolve the name into an addrinfo struct. Real DNS resolution is + # done only if resolve is true; otherwise the address must be numeric. try: - addrs = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM, socket.IPPROTO_TCP, socket.AI_NUMERICHOST) - af = addrs[0][0] + addrs = socket.getaddrinfo(host, port, af, 0, 0, gai_flags) except socket.gaierror, e: - af = 0 - if af == socket.AF_INET6: - result = u"[%s]" % host - else: - result = "%s" % host + raise ValueError("Bad host or port: \"%s\" \"%s\": %s" % (host, port, str(e))) + if not addrs: + raise ValueError("Bad host or port: \"%s\" \"%s\"" % (host, port)) + + # Convert the result of socket.getaddrinfo (which is a 2-tuple for IPv4 and + # a 4-tuple for IPv6) into a (host, port) 2-tuple. + host, port = socket.getnameinfo(addrs[0][4], socket.NI_NUMERICHOST | socket.NI_NUMERICSERV) + return host, int(port) + +def canonical_ip(host, port, af=0): + """Convert an IP address to a canonical format. Identical to resolve_to_ip, + except that the host param must already be an IP address.""" + return resolve_to_ip(host, port, af, gai_flags=socket.AI_NUMERICHOST) + +def format_addr(addr): + host, port = addr + host_str = u"" + port_str = u"" + if host is not None: + # Numeric IPv6 address? + try: + addrs = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM, socket.IPPROTO_TCP, socket.AI_NUMERICHOST) + af = addrs[0][0] + except socket.gaierror, e: + af = 0 + if af == socket.AF_INET6: + host_str = u"[%s]" % host + else: + host_str = u"%s" % host if port is not None: - result += u":%d" % port - return result + if not (0 < port <= 65535): + raise ValueError("port must be between 1 and 65535 (is %d)" % port) + port_str = u":%d" % port + + if not host_str and not port_str: + raise ValueError("host and port may not both be None") + return u"%s%s" % (host_str, port_str)