[tor-commits] [flashproxy/master] Move facilitator files into a subdirectory.

dcf at torproject.org dcf at torproject.org
Fri Aug 31 11:39:36 UTC 2012


commit 59ced2a86af9414115a594b499f7307bc7821b69
Author: David Fifield <david at bamsoftware.com>
Date:   Thu Aug 30 12:31:50 2012 -0700

    Move facilitator files into a subdirectory.
---
 Makefile                       |    3 +-
 fac.py                         |  182 ----------------------
 facilitator                    |  330 ----------------------------------------
 facilitator-test               |  124 ---------------
 facilitator.cgi                |  115 --------------
 facilitator/Makefile           |   17 ++
 facilitator/fac.py             |  182 ++++++++++++++++++++++
 facilitator/facilitator        |  330 ++++++++++++++++++++++++++++++++++++++++
 facilitator/facilitator-test   |  124 +++++++++++++++
 facilitator/facilitator.cgi    |  115 ++++++++++++++
 facilitator/init.d/facilitator |   67 ++++++++
 init.d/facilitator             |   67 --------
 12 files changed, 836 insertions(+), 820 deletions(-)

diff --git a/Makefile b/Makefile
index 3a15e54..78de922 100644
--- a/Makefile
+++ b/Makefile
@@ -10,14 +10,13 @@ all:
 
 install:
 	mkdir -p $(BINDIR)
-	cp -f flashproxy-client flashproxy-reg-http facilitator $(BINDIR)
+	cp -f flashproxy-client flashproxy-reg-http $(BINDIR)
 
 clean:
 	rm -f *.pyc
 	rm -rf dist
 
 test:
-	./facilitator-test
 	./flashproxy-client-test
 	./flashproxy-test.js
 
diff --git a/fac.py b/fac.py
deleted file mode 100644
index b9ad435..0000000
--- a/fac.py
+++ /dev/null
@@ -1,182 +0,0 @@
-import re
-import socket
-
-def parse_addr_spec(spec, defhost = None, defport = None, resolve = False):
-    """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', 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)
-
-    If resolve is true, then the host in the specification or the defhost may be
-    a domain name, which will be resolved. If resolve is false, then the host
-    must be a numeric IPv4 or IPv6 address.
-
-    IPv6 addresses must be enclosed in square brackets."""
-    host = None
-    port = None
-    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 syntax.
-    if not m:
-        m = re.match(ur'^(.+):(\d+)$', spec)
-        if m:
-            host, port = m.groups()
-            af = socket.AF_INET
-    if not m:
-        m = re.match(ur'^:?(\d+)$', spec)
-        if m:
-            port, = m.groups()
-            af = 0
-    if not m:
-        host = spec
-        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)
-
-    # Now we have split around the colon and have a guess at the address family.
-    # Forward-resolve the name into an addrinfo struct. Real DNS resolution is
-    # done only if resolve is true; otherwise the address must be numeric.
-    if resolve:
-        flags = 0
-    else:
-        flags = socket.AI_NUMERICHOST
-    try:
-        addrs = socket.getaddrinfo(host, port, af, socket.SOCK_STREAM, socket.IPPROTO_TCP, 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)
-    port = int(port)
-    return host, port
-
-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 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):
-    for k, v in params:
-        if key == k:
-            return v
-    return None
-
-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)
diff --git a/facilitator b/facilitator
deleted file mode 100755
index 3695b52..0000000
--- a/facilitator
+++ /dev/null
@@ -1,330 +0,0 @@
-#!/usr/bin/env python
-
-import SocketServer
-import errno
-import getopt
-import os
-import socket
-import sys
-import threading
-import time
-
-import fac
-
-LISTEN_ADDRESS = "127.0.0.1"
-DEFAULT_LISTEN_PORT = 9002
-DEFAULT_RELAY_PORT = 9001
-DEFAULT_LOG_FILENAME = "facilitator.log"
-
-# Don't indulge clients for more than this many seconds.
-CLIENT_TIMEOUT = 1.0
-# Buffer no many than this many bytes when trying to read a line.
-READLINE_MAX_LENGTH = 10240
-
-LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
-
-class options(object):
-    listen_port = DEFAULT_LISTEN_PORT
-    log_filename = DEFAULT_LOG_FILENAME
-    log_file = sys.stdout
-    relay_spec = None
-    daemonize = True
-    pid_filename = None
-    safe_logging = True
-
-    @staticmethod
-    def set_relay_spec(spec):
-        spec = fac.parse_addr_spec(spec, defport = DEFAULT_RELAY_PORT, resolve = True)
-        options.relay_spec = fac.format_addr(spec)
-
-def usage(f = sys.stdout):
-    print >> f, """\
-Usage: %(progname)s -r RELAY <OPTIONS>
-Flash proxy facilitator: Register client addresses and serve them out
-again. Listen on 127.0.0.1 and port PORT (by default %(port)d).
-  -d, --debug             don't daemonize, log to stdout.
-  -h, --help              show this help.
-  -l, --log FILENAME      write log to FILENAME (default \"%(log)s\").
-  -p, --port PORT         listen on PORT (by default %(port)d).
-      --pidfile FILENAME  write PID to FILENAME after daemonizing.
-  -r, --relay RELAY       send RELAY (host:port) to proxies as the relay to use.
-      --unsafe-logging    don't scrub IP addresses from logs.\
-""" % {
-    "progname": sys.argv[0],
-    "port": DEFAULT_LISTEN_PORT,
-    "log": DEFAULT_LOG_FILENAME,
-}
-
-def safe_str(s):
-    """Return s if options.safe_logging is true, and "[scrubbed]" otherwise."""
-    if options.safe_logging:
-        return "[scrubbed]"
-    else:
-        return s
-
-log_lock = threading.Lock()
-def log(msg):
-    log_lock.acquire()
-    try:
-        print >> options.log_file, (u"%s %s" % (time.strftime(LOG_DATE_FORMAT), msg)).encode("UTF-8")
-        options.log_file.flush()
-    finally:
-        log_lock.release()
-
-class TCPReg(object):
-    def __init__(self, host, port):
-        self.host = host
-        self.port = port
-
-    def __unicode__(self):
-        return fac.format_addr((self.host, self.port))
-
-    def __str__(self):
-        return unicode(self).encode("UTF-8")
-
-    def __cmp__(self, other):
-        if isinstance(other, TCPReg):
-            return cmp((self.host, self.port), (other.host, other.port))
-        else:
-            return False
-
-class Reg(object):
-    @staticmethod
-    def parse(spec, defhost = None, defport = None):
-        host, port = fac.parse_addr_spec(spec, defhost, defport)
-        return TCPReg(host, port)
-
-class RegSet(object):
-    def __init__(self):
-        self.set = []
-        self.cv = threading.Condition()
-
-    def add(self, reg):
-        self.cv.acquire()
-        try:
-            if reg not in list(self.set):
-                self.set.append(reg)
-                self.cv.notify()
-                return True
-            else:
-                return False
-        finally:
-            self.cv.release()
-
-    def fetch(self):
-        self.cv.acquire()
-        try:
-            if not self.set:
-                return None
-            return self.set.pop(0)
-        finally:
-            self.cv.release()
-
-    def __len__(self):
-        self.cv.acquire()
-        try:
-            return len(self.set)
-        finally:
-            self.cv.release()
-
-# 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
-
-class Handler(SocketServer.StreamRequestHandler):
-    def __init__(self, *args, **kwargs):
-        self.deadline = time.time() + CLIENT_TIMEOUT
-        # Buffer for readline.
-        self.buffer = ""
-        SocketServer.StreamRequestHandler.__init__(self, *args, **kwargs)
-
-    def recv(self):
-        timeout = self.deadline - time.time()
-        self.connection.settimeout(timeout)
-        return self.connection.recv(1024)
-
-    def readline(self):
-        # A line already buffered?
-        i = self.buffer.find("\n")
-        if i >= 0:
-            line = self.buffer[:i+1]
-            self.buffer = self.buffer[i+1:]
-            return line
-
-        auxbuf = []
-        buflen = len(self.buffer)
-        while True:
-            data = self.recv()
-            if not data:
-                if self.buffer or auxbuf:
-                    raise socket.error("readline: stream does not end with a newline")
-                else:
-                    return ""
-            i = data.find("\n")
-            if i >= 0:
-                line = self.buffer + "".join(auxbuf) + data[:i+1]
-                self.buffer = data[i+1:]
-                return line
-            else:
-                auxbuf.append(data)
-                buflen += len(data)
-                if buflen >= READLINE_MAX_LENGTH:
-                    raise socket.error("readline: refusing to buffer %d bytes (last read was %d bytes)" % (buflen, len(data)))
-
-    @catch_epipe
-    def handle(self):
-        num_lines = 0
-        while True:
-            try:
-                line = self.readline()
-                if not line:
-                    break
-                num_lines += 1
-            except socket.error, e:
-                log("socket error after reading %d lines: %s" % (num_lines, str(e)))
-                break
-            if not self.handle_line(line):
-                break
-
-    def handle_line(self, line):
-        if not (len(line) > 0 and line[-1] == '\n'):
-            raise ValueError("No newline at end of string returned by readline")
-        try:
-            command, params = fac.parse_transaction(line[:-1])
-        except ValueError, e:
-            log("fac.parse_transaction: %s" % e)
-            self.send_error()
-            return False
-
-        if command == "GET":
-            return self.do_GET(params)
-        if command == "PUT":
-            return self.do_PUT(params)
-        else:
-            self.send_error()
-            return False
-
-    def send_ok(self):
-        print >> self.wfile, "OK"
-
-    def send_error(self):
-        print >> self.wfile, "ERROR"
-
-    def do_GET(self, params):
-        reg = REGS.fetch()
-        if reg:
-            log(u"proxy gets %s, relay %s (now %d)" %
-                (safe_str(unicode(reg)), options.relay_spec, len(REGS)))
-            print >> self.wfile, fac.render_transaction("OK", ("CLIENT", str(reg)), ("RELAY", options.relay_spec))
-        else:
-            log(u"proxy gets none")
-            print >> self.wfile, fac.render_transaction("NONE")
-        return True
-
-    def do_PUT(self, params):
-        client_spec = fac.param_first("CLIENT", params)
-        if client_spec is None:
-            log(u"PUT missing CLIENT param")
-            self.send_error()
-            return False
-
-        # FROM
-
-        try:
-            reg = Reg.parse(client_spec, self.client_address[0])
-        except ValueError, e:
-            log(u"syntax error in %s: %s" % (safe_str(repr(client_spec)), repr(str(e))))
-            self.send_error()
-            return False
-
-        if REGS.add(reg):
-            log(u"client %s (now %d)" % (safe_str(unicode(reg)), len(REGS)))
-        else:
-            log(u"client %s (already present, now %d)" % (safe_str(unicode(reg)), len(REGS)))
-
-        self.send_ok()
-        return True
-
-    finish = catch_epipe(SocketServer.StreamRequestHandler.finish)
-
-class Server(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
-    allow_reuse_address = True
-
-REGS = RegSet()
-
-def main():
-    opts, args = getopt.gnu_getopt(sys.argv[1:], "dhl:p:r:",
-        ["debug", "help", "log=", "port=", "pidfile=", "relay=", "unsafe-logging"])
-    for o, a in opts:
-        if o == "-d" or o == "--debug":
-            options.daemonize = False
-            options.log_filename = None
-        elif o == "-h" or o == "--help":
-            usage()
-            sys.exit()
-        elif o == "-l" or o == "--log":
-            options.log_filename = a
-        elif o == "-p" or o == "--port":
-            options.listen_port = int(a)
-        elif o == "--pidfile":
-            options.pid_filename = a
-        elif o == "-r" or o == "--relay":
-            try:
-                options.set_relay_spec(a)
-            except socket.gaierror, e:
-                print >> sys.stderr, u"Can't resolve relay %s: %s" % (repr(a), str(e))
-                sys.exit(1)
-        elif o == "--unsafe-logging":
-            options.safe_logging = False
-
-    if not options.relay_spec:
-        print >> sys.stderr, """\
-The -r option is required. Give it the relay that will be sent to proxies.
-  -r HOST[:PORT]\
-    """
-        sys.exit(1)
-
-    if options.log_filename:
-        options.log_file = open(options.log_filename, "a")
-        # Send error tracebacks to the log.
-        sys.stderr = options.log_file
-    else:
-        options.log_file = sys.stdout
-
-    addrinfo = socket.getaddrinfo(LISTEN_ADDRESS, options.listen_port, 0, socket.SOCK_STREAM, socket.IPPROTO_TCP)[0]
-
-    server = Server(addrinfo[4], Handler)
-
-    log(u"start on %s" % fac.format_addr(addrinfo[4]))
-    log(u"using relay address %s" % options.relay_spec)
-
-    if options.daemonize:
-        log(u"daemonizing")
-        pid = os.fork()
-        if pid != 0:
-            if options.pid_filename:
-                f = open(options.pid_filename, "w")
-                print >> f, pid
-                f.close()
-            sys.exit(0)
-
-    try:
-        server.serve_forever()
-    except KeyboardInterrupt:
-        sys.exit(0)
-
-if __name__ == "__main__":
-    main()
diff --git a/facilitator-test b/facilitator-test
deleted file mode 100755
index b06f5d7..0000000
--- a/facilitator-test
+++ /dev/null
@@ -1,124 +0,0 @@
-#!/usr/bin/env python
-
-import socket
-import subprocess
-import time
-import unittest
-
-import fac
-
-FACILITATOR_HOST = "127.0.0.1"
-FACILITATOR_PORT = 9002
-
-def gimme_socket(host, port):
-    addrinfo = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM, socket.IPPROTO_TCP)[0]
-    s = socket.socket(addrinfo[0], addrinfo[1], addrinfo[2])
-    s.settimeout(10.0)
-    s.connect(addrinfo[4])
-    return s
-
-class FacilitatorTest(unittest.TestCase):
-    def gimme_socket(self):
-        return gimme_socket(FACILITATOR_HOST, FACILITATOR_PORT)
-
-    def setUp(self):
-        self.process = subprocess.Popen(["./facilitator", "-d", "-p", str(FACILITATOR_PORT), "-r", "0.0.1.0:1", "-l", "/dev/null"])
-        time.sleep(0.1)
-
-    def tearDown(self):
-        self.process.terminate()
-
-    def test_timeout(self):
-        """Test that the socket will not accept slow writes indefinitely.
-        Successive sends should not reset the timeout counter."""
-        s = self.gimme_socket()
-        time.sleep(0.3)
-        s.send("w")
-        time.sleep(0.3)
-        s.send("w")
-        time.sleep(0.3)
-        s.send("w")
-        time.sleep(0.3)
-        s.send("w")
-        time.sleep(0.3)
-        self.assertRaises(socket.error, s.send, "w")
-
-    def test_readline_limit(self):
-        """Test that reads won't buffer indefinitely."""
-        s = self.gimme_socket()
-        buflen = 0
-        try:
-            while buflen + 1024 < 200000:
-                s.send("X" * 1024)
-                buflen += 1024
-            self.fail("should have raised a socket error")
-        except socket.error:
-            pass
-
-#     def test_same_proxy(self):
-#         """Test that the same proxy doesn't get the same client when asking
-#         twice."""
-#         self.fail()
-#
-#     def test_num_clients(self):
-#         """Test that the same proxy can pick up up to five different clients but
-#         no more. Test that a proxy ceasing to handle a client allows the proxy
-#         to handle another, different client."""
-#         self.fail()
-#
-#     def test_num_proxies(self):
-#         """Test that a single client is handed out to five different proxies but
-#         no more. Test that a proxy ceasing to handle a client reduces its count
-#         so another proxy can handle it."""
-#         self.fail()
-#
-#     def test_proxy_timeout(self):
-#         """Test that a proxy ceasing to connect for some time period causes that
-#         proxy's clients to be unhandled by that proxy."""
-#         self.fail()
-#
-#     def test_localhost_only(self):
-#         """Test that the facilitator doesn't listen on any external
-#         addresses."""
-#         self.fail()
-#
-#     def test_hostname(self):
-#         """Test that the facilitator rejects hostnames."""
-#         self.fail()
-
-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\"")
-
-if __name__ == "__main__":
-    unittest.main()
diff --git a/facilitator.cgi b/facilitator.cgi
deleted file mode 100755
index 39566d3..0000000
--- a/facilitator.cgi
+++ /dev/null
@@ -1,115 +0,0 @@
-#!/usr/bin/env python
-
-import cgi
-import os
-import socket
-import sys
-import urllib
-
-import fac
-
-FACILITATOR_ADDR = ("127.0.0.1", 9002)
-
-def exit_error(status):
-    print """\
-Status: %d\r
-\r""" % status
-    sys.exit()
-
-def fac_socket():
-    return socket.create_connection(FACILITATOR_ADDR, 1.0).makefile()
-
-def transact(f, command, *params):
-    transaction = fac.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 fac.parse_transaction(line[:-1])
-
-def put_reg(client_addr, registrant_addr):
-    f = fac_socket()
-    try:
-        command, params = transact(f, "PUT", ("CLIENT", fac.format_addr(client_addr)), ("FROM", fac.format_addr(registrant_addr)))
-    finally:
-        f.close()
-    if command == "OK":
-        pass
-    else:
-        exit_error(500)
-
-def get_reg(proxy_addr):
-    f = fac_socket()
-    try:
-        command, params = transact(f, "GET", ("FROM", fac.format_addr(proxy_addr)))
-    finally:
-        f.close()
-    if command == "NONE":
-        return {
-            "client": ""
-        }
-    elif command == "OK":
-        client_spec = fac.param_first("CLIENT", params)
-        relay_spec = fac.param_first("RELAY", params)
-        if not client_spec or not relay_spec:
-            exit_error(500)
-        try:
-            # Check the syntax returned by the backend.
-            client = fac.parse_addr_spec(client_spec)
-            relay = fac.parse_addr_spec(relay_spec)
-        except ValueError:
-            exit_error(500)
-        return {
-            "client": fac.format_addr(client),
-            "relay": fac.format_addr(relay),
-        }
-    else:
-        exit_error(500)
-
-method = os.environ.get("REQUEST_METHOD")
-proxy_addr = (os.environ.get("REMOTE_ADDR"), None)
-
-if not method or not proxy_addr[0]:
-    exit_error(400)
-
-fs = cgi.FieldStorage()
-
-def do_get():
-    try:
-        reg = get_reg(proxy_addr) or ""
-    except:
-        exit_error(500)
-    # Allow XMLHttpRequest from any domain. http://www.w3.org/TR/cors/.
-    print """\
-Status: 200\r
-Content-Type: application/x-www-form-urlencoded\r
-Cache-Control: no-cache\r
-Access-Control-Allow-Origin: *\r
-\r"""
-    sys.stdout.write(urllib.urlencode(reg))
-
-def do_post():
-    client_specs = fs.getlist("client")
-    if len(client_specs) != 1:
-        exit_error(400)
-    client_spec = client_specs[0]
-    try:
-        client_addr = fac.parse_addr_spec(client_spec, defhost=proxy_addr[0])
-    except ValueError:
-        exit_error(400)
-    try:
-        put_reg(client_addr, proxy_addr)
-    except:
-        raise
-        exit_error(500)
-    print """\
-Status: 200\r
-\r"""
-
-if method == "GET":
-    do_get()
-elif method == "POST":
-    do_post()
-else:
-    exit_error(405)
diff --git a/facilitator/Makefile b/facilitator/Makefile
new file mode 100644
index 0000000..1d9cb54
--- /dev/null
+++ b/facilitator/Makefile
@@ -0,0 +1,17 @@
+PREFIX = /usr/local
+BINDIR = $(PREFIX)/bin
+
+all:
+	:
+
+install:
+	mkdir -p $(BINDIR)
+	cp -f facilitator facilitator.cgi fac.py $(BINDIR)
+
+clean:
+	rm -f *.pyc
+
+test:
+	./facilitator-test
+
+.PHONY: all install clean test
diff --git a/facilitator/fac.py b/facilitator/fac.py
new file mode 100644
index 0000000..b9ad435
--- /dev/null
+++ b/facilitator/fac.py
@@ -0,0 +1,182 @@
+import re
+import socket
+
+def parse_addr_spec(spec, defhost = None, defport = None, resolve = False):
+    """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', 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)
+
+    If resolve is true, then the host in the specification or the defhost may be
+    a domain name, which will be resolved. If resolve is false, then the host
+    must be a numeric IPv4 or IPv6 address.
+
+    IPv6 addresses must be enclosed in square brackets."""
+    host = None
+    port = None
+    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 syntax.
+    if not m:
+        m = re.match(ur'^(.+):(\d+)$', spec)
+        if m:
+            host, port = m.groups()
+            af = socket.AF_INET
+    if not m:
+        m = re.match(ur'^:?(\d+)$', spec)
+        if m:
+            port, = m.groups()
+            af = 0
+    if not m:
+        host = spec
+        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)
+
+    # Now we have split around the colon and have a guess at the address family.
+    # Forward-resolve the name into an addrinfo struct. Real DNS resolution is
+    # done only if resolve is true; otherwise the address must be numeric.
+    if resolve:
+        flags = 0
+    else:
+        flags = socket.AI_NUMERICHOST
+    try:
+        addrs = socket.getaddrinfo(host, port, af, socket.SOCK_STREAM, socket.IPPROTO_TCP, 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)
+    port = int(port)
+    return host, port
+
+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 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):
+    for k, v in params:
+        if key == k:
+            return v
+    return None
+
+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)
diff --git a/facilitator/facilitator b/facilitator/facilitator
new file mode 100755
index 0000000..3695b52
--- /dev/null
+++ b/facilitator/facilitator
@@ -0,0 +1,330 @@
+#!/usr/bin/env python
+
+import SocketServer
+import errno
+import getopt
+import os
+import socket
+import sys
+import threading
+import time
+
+import fac
+
+LISTEN_ADDRESS = "127.0.0.1"
+DEFAULT_LISTEN_PORT = 9002
+DEFAULT_RELAY_PORT = 9001
+DEFAULT_LOG_FILENAME = "facilitator.log"
+
+# Don't indulge clients for more than this many seconds.
+CLIENT_TIMEOUT = 1.0
+# Buffer no many than this many bytes when trying to read a line.
+READLINE_MAX_LENGTH = 10240
+
+LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
+
+class options(object):
+    listen_port = DEFAULT_LISTEN_PORT
+    log_filename = DEFAULT_LOG_FILENAME
+    log_file = sys.stdout
+    relay_spec = None
+    daemonize = True
+    pid_filename = None
+    safe_logging = True
+
+    @staticmethod
+    def set_relay_spec(spec):
+        spec = fac.parse_addr_spec(spec, defport = DEFAULT_RELAY_PORT, resolve = True)
+        options.relay_spec = fac.format_addr(spec)
+
+def usage(f = sys.stdout):
+    print >> f, """\
+Usage: %(progname)s -r RELAY <OPTIONS>
+Flash proxy facilitator: Register client addresses and serve them out
+again. Listen on 127.0.0.1 and port PORT (by default %(port)d).
+  -d, --debug             don't daemonize, log to stdout.
+  -h, --help              show this help.
+  -l, --log FILENAME      write log to FILENAME (default \"%(log)s\").
+  -p, --port PORT         listen on PORT (by default %(port)d).
+      --pidfile FILENAME  write PID to FILENAME after daemonizing.
+  -r, --relay RELAY       send RELAY (host:port) to proxies as the relay to use.
+      --unsafe-logging    don't scrub IP addresses from logs.\
+""" % {
+    "progname": sys.argv[0],
+    "port": DEFAULT_LISTEN_PORT,
+    "log": DEFAULT_LOG_FILENAME,
+}
+
+def safe_str(s):
+    """Return s if options.safe_logging is true, and "[scrubbed]" otherwise."""
+    if options.safe_logging:
+        return "[scrubbed]"
+    else:
+        return s
+
+log_lock = threading.Lock()
+def log(msg):
+    log_lock.acquire()
+    try:
+        print >> options.log_file, (u"%s %s" % (time.strftime(LOG_DATE_FORMAT), msg)).encode("UTF-8")
+        options.log_file.flush()
+    finally:
+        log_lock.release()
+
+class TCPReg(object):
+    def __init__(self, host, port):
+        self.host = host
+        self.port = port
+
+    def __unicode__(self):
+        return fac.format_addr((self.host, self.port))
+
+    def __str__(self):
+        return unicode(self).encode("UTF-8")
+
+    def __cmp__(self, other):
+        if isinstance(other, TCPReg):
+            return cmp((self.host, self.port), (other.host, other.port))
+        else:
+            return False
+
+class Reg(object):
+    @staticmethod
+    def parse(spec, defhost = None, defport = None):
+        host, port = fac.parse_addr_spec(spec, defhost, defport)
+        return TCPReg(host, port)
+
+class RegSet(object):
+    def __init__(self):
+        self.set = []
+        self.cv = threading.Condition()
+
+    def add(self, reg):
+        self.cv.acquire()
+        try:
+            if reg not in list(self.set):
+                self.set.append(reg)
+                self.cv.notify()
+                return True
+            else:
+                return False
+        finally:
+            self.cv.release()
+
+    def fetch(self):
+        self.cv.acquire()
+        try:
+            if not self.set:
+                return None
+            return self.set.pop(0)
+        finally:
+            self.cv.release()
+
+    def __len__(self):
+        self.cv.acquire()
+        try:
+            return len(self.set)
+        finally:
+            self.cv.release()
+
+# 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
+
+class Handler(SocketServer.StreamRequestHandler):
+    def __init__(self, *args, **kwargs):
+        self.deadline = time.time() + CLIENT_TIMEOUT
+        # Buffer for readline.
+        self.buffer = ""
+        SocketServer.StreamRequestHandler.__init__(self, *args, **kwargs)
+
+    def recv(self):
+        timeout = self.deadline - time.time()
+        self.connection.settimeout(timeout)
+        return self.connection.recv(1024)
+
+    def readline(self):
+        # A line already buffered?
+        i = self.buffer.find("\n")
+        if i >= 0:
+            line = self.buffer[:i+1]
+            self.buffer = self.buffer[i+1:]
+            return line
+
+        auxbuf = []
+        buflen = len(self.buffer)
+        while True:
+            data = self.recv()
+            if not data:
+                if self.buffer or auxbuf:
+                    raise socket.error("readline: stream does not end with a newline")
+                else:
+                    return ""
+            i = data.find("\n")
+            if i >= 0:
+                line = self.buffer + "".join(auxbuf) + data[:i+1]
+                self.buffer = data[i+1:]
+                return line
+            else:
+                auxbuf.append(data)
+                buflen += len(data)
+                if buflen >= READLINE_MAX_LENGTH:
+                    raise socket.error("readline: refusing to buffer %d bytes (last read was %d bytes)" % (buflen, len(data)))
+
+    @catch_epipe
+    def handle(self):
+        num_lines = 0
+        while True:
+            try:
+                line = self.readline()
+                if not line:
+                    break
+                num_lines += 1
+            except socket.error, e:
+                log("socket error after reading %d lines: %s" % (num_lines, str(e)))
+                break
+            if not self.handle_line(line):
+                break
+
+    def handle_line(self, line):
+        if not (len(line) > 0 and line[-1] == '\n'):
+            raise ValueError("No newline at end of string returned by readline")
+        try:
+            command, params = fac.parse_transaction(line[:-1])
+        except ValueError, e:
+            log("fac.parse_transaction: %s" % e)
+            self.send_error()
+            return False
+
+        if command == "GET":
+            return self.do_GET(params)
+        if command == "PUT":
+            return self.do_PUT(params)
+        else:
+            self.send_error()
+            return False
+
+    def send_ok(self):
+        print >> self.wfile, "OK"
+
+    def send_error(self):
+        print >> self.wfile, "ERROR"
+
+    def do_GET(self, params):
+        reg = REGS.fetch()
+        if reg:
+            log(u"proxy gets %s, relay %s (now %d)" %
+                (safe_str(unicode(reg)), options.relay_spec, len(REGS)))
+            print >> self.wfile, fac.render_transaction("OK", ("CLIENT", str(reg)), ("RELAY", options.relay_spec))
+        else:
+            log(u"proxy gets none")
+            print >> self.wfile, fac.render_transaction("NONE")
+        return True
+
+    def do_PUT(self, params):
+        client_spec = fac.param_first("CLIENT", params)
+        if client_spec is None:
+            log(u"PUT missing CLIENT param")
+            self.send_error()
+            return False
+
+        # FROM
+
+        try:
+            reg = Reg.parse(client_spec, self.client_address[0])
+        except ValueError, e:
+            log(u"syntax error in %s: %s" % (safe_str(repr(client_spec)), repr(str(e))))
+            self.send_error()
+            return False
+
+        if REGS.add(reg):
+            log(u"client %s (now %d)" % (safe_str(unicode(reg)), len(REGS)))
+        else:
+            log(u"client %s (already present, now %d)" % (safe_str(unicode(reg)), len(REGS)))
+
+        self.send_ok()
+        return True
+
+    finish = catch_epipe(SocketServer.StreamRequestHandler.finish)
+
+class Server(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
+    allow_reuse_address = True
+
+REGS = RegSet()
+
+def main():
+    opts, args = getopt.gnu_getopt(sys.argv[1:], "dhl:p:r:",
+        ["debug", "help", "log=", "port=", "pidfile=", "relay=", "unsafe-logging"])
+    for o, a in opts:
+        if o == "-d" or o == "--debug":
+            options.daemonize = False
+            options.log_filename = None
+        elif o == "-h" or o == "--help":
+            usage()
+            sys.exit()
+        elif o == "-l" or o == "--log":
+            options.log_filename = a
+        elif o == "-p" or o == "--port":
+            options.listen_port = int(a)
+        elif o == "--pidfile":
+            options.pid_filename = a
+        elif o == "-r" or o == "--relay":
+            try:
+                options.set_relay_spec(a)
+            except socket.gaierror, e:
+                print >> sys.stderr, u"Can't resolve relay %s: %s" % (repr(a), str(e))
+                sys.exit(1)
+        elif o == "--unsafe-logging":
+            options.safe_logging = False
+
+    if not options.relay_spec:
+        print >> sys.stderr, """\
+The -r option is required. Give it the relay that will be sent to proxies.
+  -r HOST[:PORT]\
+    """
+        sys.exit(1)
+
+    if options.log_filename:
+        options.log_file = open(options.log_filename, "a")
+        # Send error tracebacks to the log.
+        sys.stderr = options.log_file
+    else:
+        options.log_file = sys.stdout
+
+    addrinfo = socket.getaddrinfo(LISTEN_ADDRESS, options.listen_port, 0, socket.SOCK_STREAM, socket.IPPROTO_TCP)[0]
+
+    server = Server(addrinfo[4], Handler)
+
+    log(u"start on %s" % fac.format_addr(addrinfo[4]))
+    log(u"using relay address %s" % options.relay_spec)
+
+    if options.daemonize:
+        log(u"daemonizing")
+        pid = os.fork()
+        if pid != 0:
+            if options.pid_filename:
+                f = open(options.pid_filename, "w")
+                print >> f, pid
+                f.close()
+            sys.exit(0)
+
+    try:
+        server.serve_forever()
+    except KeyboardInterrupt:
+        sys.exit(0)
+
+if __name__ == "__main__":
+    main()
diff --git a/facilitator/facilitator-test b/facilitator/facilitator-test
new file mode 100755
index 0000000..b06f5d7
--- /dev/null
+++ b/facilitator/facilitator-test
@@ -0,0 +1,124 @@
+#!/usr/bin/env python
+
+import socket
+import subprocess
+import time
+import unittest
+
+import fac
+
+FACILITATOR_HOST = "127.0.0.1"
+FACILITATOR_PORT = 9002
+
+def gimme_socket(host, port):
+    addrinfo = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM, socket.IPPROTO_TCP)[0]
+    s = socket.socket(addrinfo[0], addrinfo[1], addrinfo[2])
+    s.settimeout(10.0)
+    s.connect(addrinfo[4])
+    return s
+
+class FacilitatorTest(unittest.TestCase):
+    def gimme_socket(self):
+        return gimme_socket(FACILITATOR_HOST, FACILITATOR_PORT)
+
+    def setUp(self):
+        self.process = subprocess.Popen(["./facilitator", "-d", "-p", str(FACILITATOR_PORT), "-r", "0.0.1.0:1", "-l", "/dev/null"])
+        time.sleep(0.1)
+
+    def tearDown(self):
+        self.process.terminate()
+
+    def test_timeout(self):
+        """Test that the socket will not accept slow writes indefinitely.
+        Successive sends should not reset the timeout counter."""
+        s = self.gimme_socket()
+        time.sleep(0.3)
+        s.send("w")
+        time.sleep(0.3)
+        s.send("w")
+        time.sleep(0.3)
+        s.send("w")
+        time.sleep(0.3)
+        s.send("w")
+        time.sleep(0.3)
+        self.assertRaises(socket.error, s.send, "w")
+
+    def test_readline_limit(self):
+        """Test that reads won't buffer indefinitely."""
+        s = self.gimme_socket()
+        buflen = 0
+        try:
+            while buflen + 1024 < 200000:
+                s.send("X" * 1024)
+                buflen += 1024
+            self.fail("should have raised a socket error")
+        except socket.error:
+            pass
+
+#     def test_same_proxy(self):
+#         """Test that the same proxy doesn't get the same client when asking
+#         twice."""
+#         self.fail()
+#
+#     def test_num_clients(self):
+#         """Test that the same proxy can pick up up to five different clients but
+#         no more. Test that a proxy ceasing to handle a client allows the proxy
+#         to handle another, different client."""
+#         self.fail()
+#
+#     def test_num_proxies(self):
+#         """Test that a single client is handed out to five different proxies but
+#         no more. Test that a proxy ceasing to handle a client reduces its count
+#         so another proxy can handle it."""
+#         self.fail()
+#
+#     def test_proxy_timeout(self):
+#         """Test that a proxy ceasing to connect for some time period causes that
+#         proxy's clients to be unhandled by that proxy."""
+#         self.fail()
+#
+#     def test_localhost_only(self):
+#         """Test that the facilitator doesn't listen on any external
+#         addresses."""
+#         self.fail()
+#
+#     def test_hostname(self):
+#         """Test that the facilitator rejects hostnames."""
+#         self.fail()
+
+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\"")
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/facilitator/facilitator.cgi b/facilitator/facilitator.cgi
new file mode 100755
index 0000000..39566d3
--- /dev/null
+++ b/facilitator/facilitator.cgi
@@ -0,0 +1,115 @@
+#!/usr/bin/env python
+
+import cgi
+import os
+import socket
+import sys
+import urllib
+
+import fac
+
+FACILITATOR_ADDR = ("127.0.0.1", 9002)
+
+def exit_error(status):
+    print """\
+Status: %d\r
+\r""" % status
+    sys.exit()
+
+def fac_socket():
+    return socket.create_connection(FACILITATOR_ADDR, 1.0).makefile()
+
+def transact(f, command, *params):
+    transaction = fac.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 fac.parse_transaction(line[:-1])
+
+def put_reg(client_addr, registrant_addr):
+    f = fac_socket()
+    try:
+        command, params = transact(f, "PUT", ("CLIENT", fac.format_addr(client_addr)), ("FROM", fac.format_addr(registrant_addr)))
+    finally:
+        f.close()
+    if command == "OK":
+        pass
+    else:
+        exit_error(500)
+
+def get_reg(proxy_addr):
+    f = fac_socket()
+    try:
+        command, params = transact(f, "GET", ("FROM", fac.format_addr(proxy_addr)))
+    finally:
+        f.close()
+    if command == "NONE":
+        return {
+            "client": ""
+        }
+    elif command == "OK":
+        client_spec = fac.param_first("CLIENT", params)
+        relay_spec = fac.param_first("RELAY", params)
+        if not client_spec or not relay_spec:
+            exit_error(500)
+        try:
+            # Check the syntax returned by the backend.
+            client = fac.parse_addr_spec(client_spec)
+            relay = fac.parse_addr_spec(relay_spec)
+        except ValueError:
+            exit_error(500)
+        return {
+            "client": fac.format_addr(client),
+            "relay": fac.format_addr(relay),
+        }
+    else:
+        exit_error(500)
+
+method = os.environ.get("REQUEST_METHOD")
+proxy_addr = (os.environ.get("REMOTE_ADDR"), None)
+
+if not method or not proxy_addr[0]:
+    exit_error(400)
+
+fs = cgi.FieldStorage()
+
+def do_get():
+    try:
+        reg = get_reg(proxy_addr) or ""
+    except:
+        exit_error(500)
+    # Allow XMLHttpRequest from any domain. http://www.w3.org/TR/cors/.
+    print """\
+Status: 200\r
+Content-Type: application/x-www-form-urlencoded\r
+Cache-Control: no-cache\r
+Access-Control-Allow-Origin: *\r
+\r"""
+    sys.stdout.write(urllib.urlencode(reg))
+
+def do_post():
+    client_specs = fs.getlist("client")
+    if len(client_specs) != 1:
+        exit_error(400)
+    client_spec = client_specs[0]
+    try:
+        client_addr = fac.parse_addr_spec(client_spec, defhost=proxy_addr[0])
+    except ValueError:
+        exit_error(400)
+    try:
+        put_reg(client_addr, proxy_addr)
+    except:
+        raise
+        exit_error(500)
+    print """\
+Status: 200\r
+\r"""
+
+if method == "GET":
+    do_get()
+elif method == "POST":
+    do_post()
+else:
+    exit_error(405)
diff --git a/facilitator/init.d/facilitator b/facilitator/init.d/facilitator
new file mode 100755
index 0000000..5510d11
--- /dev/null
+++ b/facilitator/init.d/facilitator
@@ -0,0 +1,67 @@
+#!/bin/sh
+#
+# facilitator  This shell script takes care of starting and stopping
+#              the flash proxy facilitator.
+#
+# chkconfig: 2345 90 10
+# description: Flash proxy facilitator.
+# processname: facilitator
+# pidfile: /var/flashproxy/facilitator.pid
+
+# Installation instructions:
+# cp facilitator /etc/init.d/facilitator
+# chkconfig --add facilitator
+# service facilitator start
+
+# Source function library.
+. /etc/rc.d/init.d/functions
+
+# Replace this with the address of a Tor relay with a websocket pluggable
+# transport. You can use host:port syntax.
+RELAY=tor1.bamsoftware.com:9901
+
+BINDIR=/usr/local/bin
+VARDIR=/var/flashproxy
+PROG=$BINDIR/facilitator
+PIDFILE=$VARDIR/facilitator.pid
+USER=flashproxy
+
+# See how we were called.
+case "$1" in
+  start)
+	[ -x $PROG ] || exit 1
+	echo -n $"Starting flash proxy facilitator: "
+	cd $VARDIR && daemon --user $USER --pidfile $PIDFILE $PROG --pidfile $PIDFILE -r $RELAY
+	RETVAL=$?
+	echo
+	[ $RETVAL -eq 0 ] && touch /var/lock/subsys/facilitator
+	;;
+  stop)
+	# Stop daemon.
+	echo -n $"Shutting down flash proxy facilitator: "
+	killproc -p $PIDFILE
+	RETVAL=$?
+	echo
+	if [ $RETVAL -eq 0 ]; then
+		rm -f /var/lock/subsys/facilitator
+		rm -f $PIDFILE
+	fi
+	;;
+  status)
+	status -p $PIDFILE facilitator
+	RETVAL=$?
+	;;
+  restart|reload)
+	$0 stop
+	$0 start
+	;;
+  condrestart)
+	[ -f /var/lock/subsys/facilitator ] && restart || :
+	;;
+  *)
+	echo $"Usage: $0 {start|stop|status|restart}"
+	RETVAL=3
+	;;
+esac
+
+exit $RETVAL
diff --git a/init.d/facilitator b/init.d/facilitator
deleted file mode 100755
index 5510d11..0000000
--- a/init.d/facilitator
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/bin/sh
-#
-# facilitator  This shell script takes care of starting and stopping
-#              the flash proxy facilitator.
-#
-# chkconfig: 2345 90 10
-# description: Flash proxy facilitator.
-# processname: facilitator
-# pidfile: /var/flashproxy/facilitator.pid
-
-# Installation instructions:
-# cp facilitator /etc/init.d/facilitator
-# chkconfig --add facilitator
-# service facilitator start
-
-# Source function library.
-. /etc/rc.d/init.d/functions
-
-# Replace this with the address of a Tor relay with a websocket pluggable
-# transport. You can use host:port syntax.
-RELAY=tor1.bamsoftware.com:9901
-
-BINDIR=/usr/local/bin
-VARDIR=/var/flashproxy
-PROG=$BINDIR/facilitator
-PIDFILE=$VARDIR/facilitator.pid
-USER=flashproxy
-
-# See how we were called.
-case "$1" in
-  start)
-	[ -x $PROG ] || exit 1
-	echo -n $"Starting flash proxy facilitator: "
-	cd $VARDIR && daemon --user $USER --pidfile $PIDFILE $PROG --pidfile $PIDFILE -r $RELAY
-	RETVAL=$?
-	echo
-	[ $RETVAL -eq 0 ] && touch /var/lock/subsys/facilitator
-	;;
-  stop)
-	# Stop daemon.
-	echo -n $"Shutting down flash proxy facilitator: "
-	killproc -p $PIDFILE
-	RETVAL=$?
-	echo
-	if [ $RETVAL -eq 0 ]; then
-		rm -f /var/lock/subsys/facilitator
-		rm -f $PIDFILE
-	fi
-	;;
-  status)
-	status -p $PIDFILE facilitator
-	RETVAL=$?
-	;;
-  restart|reload)
-	$0 stop
-	$0 start
-	;;
-  condrestart)
-	[ -f /var/lock/subsys/facilitator ] && restart || :
-	;;
-  *)
-	echo $"Usage: $0 {start|stop|status|restart}"
-	RETVAL=3
-	;;
-esac
-
-exit $RETVAL





More information about the tor-commits mailing list