[tor-commits] [flashproxy/master] Move flash proxy badge files into a subdirectory.

dcf at torproject.org dcf at torproject.org
Sat Sep 29 23:24:36 UTC 2012


commit c8bd5fd4fed1907817087ed8f7a6cba6511d1fb5
Author: David Fifield <david at bamsoftware.com>
Date:   Fri Sep 28 19:43:32 2012 -0700

    Move flash proxy badge files into a subdirectory.
    
    Now the root contains only files of interest to the most likely end
    user: a client and not a web site owner, relay operator, or facilitator
    operator.
---
 Makefile                 |    1 -
 badge.png                |  Bin 254 -> 0 bytes
 badge.xcf                |  Bin 1441 -> 0 bytes
 embed.html               |   87 -----
 flashproxy-test.js       |  239 ------------
 flashproxy.js            |  898 ----------------------------------------------
 proxy/README             |    4 +
 proxy/badge.png          |  Bin 0 -> 254 bytes
 proxy/badge.xcf          |  Bin 0 -> 1441 bytes
 proxy/embed.html         |   87 +++++
 proxy/flashproxy-test.js |  239 ++++++++++++
 proxy/flashproxy.js      |  898 ++++++++++++++++++++++++++++++++++++++++++++++
 12 files changed, 1228 insertions(+), 1225 deletions(-)

diff --git a/Makefile b/Makefile
index 0effce4..55c5859 100644
--- a/Makefile
+++ b/Makefile
@@ -18,7 +18,6 @@ clean:
 
 test:
 	./flashproxy-client-test
-	./flashproxy-test.js
 
 DISTNAME = flashproxy-client-$(VERSION)
 DISTDIR = dist/$(DISTNAME)
diff --git a/badge.png b/badge.png
deleted file mode 100644
index 068cf78..0000000
Binary files a/badge.png and /dev/null differ
diff --git a/badge.xcf b/badge.xcf
deleted file mode 100644
index d12ff1f..0000000
Binary files a/badge.xcf and /dev/null differ
diff --git a/embed.html b/embed.html
deleted file mode 100644
index 95821ce..0000000
--- a/embed.html
+++ /dev/null
@@ -1,87 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-<meta charset="utf-8">
-<meta http-equiv="refresh" content="86400">
-<style type="text/css">
-html {
-	width: 100%;
-	height: 100%;
-}
-body {
-	margin: 0;
-	padding: 0;
-	width: 100%;
-	height: 100%;
-}
-#flashproxy-badge {
-	width: 100%;
-	height: 100%;
-	margin: 0;
-	padding: 0;
-	border: 0;
-	border-collapse: collapse;
-	line-height: 0;
-}
-#flashproxy-badge.idle {
-	background-color: #227;
-}
-#flashproxy-badge.active {
-	background-color: #28f;
-}
-#flashproxy-badge.disabled {
-	background-color: #777;
-}
-#flashproxy-badge.dead {
-	background-color: #111;
-}
-#flashproxy-badge td {
-	position: relative;
-	margin: 0;
-	padding: 0;
-	vertical-align: middle;
-	text-align: center;
-}
-#flashproxy-badge td .disable-button {
-	position: absolute;
-	margin: 0;
-	padding: 0;
-	border: 1px solid white;
-	color: white;
-	background-color: #B00;
-	top: 1px;
-	right: 1px;
-	width: 11px;
-	height: 11px;
-	text-align: center;
-	line-height: 11px;
-	font-size: 11px;
-	display: none;
-}
-#flashproxy-badge a img {
-	border: 0;
-}
-#flashproxy-badge.debug {
-	position: absolute;
-	margin: 0;
-	left: 0;
-	top: 0;
-	width: 100%;
-	height: 100%;
-	overflow: auto;
-	color: #4c4;
-	background-color: #021;
-	line-height: inherit;
-}
-</style>
-</head>
-<body>
-<script type="text/javascript" src="flashproxy.js"></script>
-<script type="text/javascript">
-flashproxy_badge_insert().start();
-</script>
-<noscript>
-<table id="flashproxy-badge" class="disabled"><tr><td><a href="//crypto.stanford.edu/flashproxy/" target="_parent"><img src="badge.png" alt="Internet freedom"></a></td></tr></table>
-</noscript>
-</body>
-</html>
diff --git a/flashproxy-test.js b/flashproxy-test.js
deleted file mode 100755
index 2dc185a..0000000
--- a/flashproxy-test.js
+++ /dev/null
@@ -1,239 +0,0 @@
-#!/usr/bin/js
-
-/* To run this test program, install the Rhino JavaScript interpreter
-   (apt-get install rhino). */
-
-var VERBOSE = false;
-if ("-v" in arguments)
-    VERBOSE = true;
-
-var num_tests = 0;
-var num_failed = 0;
-
-var window = {location: {search: "?"}};
-
-load("flashproxy.js");
-
-function objects_equal(a, b)
-{
-    if ((a === null) != (b === null))
-        return false;
-    if (typeof a != typeof b)
-        return false;
-    if (typeof a != "object")
-        return a == b;
-
-    for (var k in a) {
-        if (!objects_equal(a[k], b[k]))
-            return false;
-    }
-    for (var k in b) {
-        if (!objects_equal(a[k], b[k]))
-            return false;
-    }
-
-    return true;
-}
-
-var top = true;
-function announce(test_name)
-{
-    if (VERBOSE) {
-        if (!top)
-            print();
-        print(test_name);
-    }
-    top = false;
-}
-
-function pass(test)
-{
-    num_tests++;
-    if (VERBOSE)
-        print("PASS " + repr(test));
-}
-
-function fail(test, expected, actual)
-{
-    num_tests++;
-    num_failed++;
-    print("FAIL " + repr(test) + "  expected: " + repr(expected) + "  actual: " + repr(actual));
-}
-
-function test_build_url()
-{
-    var TESTS = [
-        { args: ["http", "example.com"],
-          expected: "http://example.com" },
-        { args: ["http", "example.com", 80],
-          expected: "http://example.com" },
-        { args: ["http", "example.com", 81],
-          expected: "http://example.com:81" },
-        { args: ["https", "example.com", 443],
-          expected: "https://example.com" },
-        { args: ["https", "example.com", 444],
-          expected: "https://example.com:444" },
-        { args: ["http", "example.com", 80, "/"],
-          expected: "http://example.com/" },
-        { args: ["http", "example.com", 80, "/test?k=%#v"],
-          expected: "http://example.com/test%3Fk%3D%25%23v" },
-        { args: ["http", "example.com", 80, "/test", []],
-          expected: "http://example.com/test?" },
-        { args: ["http", "example.com", 80, "/test", [["k", "%#v"]]],
-          expected: "http://example.com/test?k=%25%23v" },
-        { args: ["http", "example.com", 80, "/test", [["a", "b"], ["c", "d"]]],
-          expected: "http://example.com/test?a=b&c=d" },
-        { args: ["http", "1.2.3.4"],
-          expected: "http://1.2.3.4" },
-        { args: ["http", "1:2::3:4"],
-          expected: "http://[1:2::3:4]" },
-        { args: ["http", "bog][us"],
-          expected: "http://bog%5D%5Bus" },
-        { args: ["http", "bog:u]s"],
-          expected: "http://bog%3Au%5Ds" },
-    ];
-
-    announce("test_build_url");
-    for (var i = 0; i < TESTS.length; i++) {
-        var test = TESTS[i];
-        var actual;
-
-        actual = build_url.apply(undefined, test.args);
-        if (objects_equal(actual, test.expected))
-            pass(test.args);
-        else
-            fail(test.args, test.expected, actual);
-    }
-}
-
-function test_parse_query_string()
-{
-    var TESTS = [
-        { qs: "",
-          expected: { } },
-        { qs: "a=b",
-          expected: { a: "b" } },
-        { qs: "a=b=c",
-          expected: { a: "b=c" } },
-        { qs: "a=b&c=d",
-          expected: { a: "b", c: "d" } },
-        { qs: "client=&relay=1.2.3.4%3A9001",
-          expected: { client: "", relay: "1.2.3.4:9001" } },
-        { qs: "a=b%26c=d",
-          expected: { a: "b&c=d" } },
-        { qs: "a%3db=d",
-          expected: { "a=b": "d" } },
-        { qs: "a=b+c%20d",
-          expected: { "a": "b c d" } },
-        { qs: "a=b+c%2bd",
-          expected: { "a": "b c+d" } },
-        { qs: "a+b=c",
-          expected: { "a b": "c" } },
-        { qs: "a=b+c+d",
-          expected: { a: "b c d" } },
-        /* First appearance wins. */
-        { qs: "a=b&c=d&a=e",
-          expected: { a: "b", c: "d" } },
-        { qs: "a",
-          expected: { a: "" } },
-        { qs: "=b",
-          expected: { "": "b" } },
-        { qs: "&a=b",
-          expected: { "": "", a: "b" } },
-        { qs: "a=b&",
-          expected: { "": "", a: "b" } },
-        { qs: "a=b&&c=d",
-          expected: { "": "", a: "b", c: "d" } },
-    ];
-
-    announce("test_parse_query_string");
-    for (var i = 0; i < TESTS.length; i++) {
-        var test = TESTS[i];
-        var actual;
-
-        actual = parse_query_string(test.qs);
-        if (objects_equal(actual, test.expected))
-            pass(test.qs);
-        else
-            fail(test.qs, test.expected, actual);
-    }
-}
-
-function test_parse_addr_spec()
-{
-    var TESTS = [
-        { spec: "",
-          expected: null },
-        { spec: "3.3.3.3:4444",
-          expected: { host: "3.3.3.3", port: 4444 } },
-        { spec: "3.3.3.3",
-          expected: null },
-        { spec: "3.3.3.3:0x1111",
-          expected: null },
-        { spec: "3.3.3.3:-4444",
-          expected: null },
-        { spec: "3.3.3.3:65536",
-          expected: null },
-        { spec: "[1:2::a:f]:4444",
-          expected: { host: "1:2::a:f", port: 4444 } },
-        { spec: "[1:2::a:f]",
-          expected: null },
-        { spec: "[1:2::a:f]:0x1111",
-          expected: null },
-        { spec: "[1:2::a:f]:-4444",
-          expected: null },
-        { spec: "[1:2::a:f]:65536",
-          expected: null },
-        { spec: "[1:2::ffff:1.2.3.4]:4444",
-          expected: { host: "1:2::ffff:1.2.3.4", port: 4444 } },
-    ];
-
-    announce("test_parse_addr_spec");
-    for (var i = 0; i < TESTS.length; i++) {
-        var test = TESTS[i];
-        var actual;
-
-        actual = parse_addr_spec(test.spec);
-        if (objects_equal(actual, test.expected))
-            pass(test.spec);
-        else
-            fail(test.spec, test.expected, actual);
-    }
-}
-
-function test_get_query_param_addr()
-{
-    var DEFAULT = { host: "1.1.1.1", port: 2222 };
-    var TESTS = [
-        { query: { },
-          expected: DEFAULT },
-        { query: { addr: "3.3.3.3:4444" },
-          expected: { host: "3.3.3.3", port: 4444 } },
-        { query: { x: "3.3.3.3:4444" },
-          expected: DEFAULT },
-        { query: { addr: "---" },
-          expected: null },
-    ];
-
-    announce("test_get_query_param_addr");
-    for (var i = 0; i < TESTS.length; i++) {
-        var test = TESTS[i];
-        var actual;
-
-        actual = get_query_param_addr(test.query, "addr", DEFAULT);
-        if (objects_equal(actual, test.expected))
-            pass(test.query);
-        else
-            fail(test.query, test.expected, actual);
-    }
-}
-
-test_build_url();
-test_parse_query_string();
-test_parse_addr_spec();
-test_get_query_param_addr();
-
-if (num_failed == 0)
-    quit(0);
-else
-    quit(1);
diff --git a/flashproxy.js b/flashproxy.js
deleted file mode 100644
index a2e8863..0000000
--- a/flashproxy.js
+++ /dev/null
@@ -1,898 +0,0 @@
-/* Query string parameters. These change how the program runs from the outside.
- * For example:
- *   http://www.example.com/embed.html?facilitator=127.0.0.1:9002&debug=1
- *
- * client=<HOST>:<PORT>
- * The address of the client to connect to. The proxy normally receives this
- * information from the facilitator. When this option is used, the facilitator
- * query is not done. The "relay" parameter must be given as well.
- *
- * debug=1
- * If set (to any value), show verbose terminal-like output instead of the
- * badge.
- *
- * facilitator=https://host:port/
- * The URL of the facilitator CGI script. By default it is
- * DEFAULT_FACILITATOR_URL.
- *
- * facilitator_poll_interval=<FLOAT>
- * How often to poll the facilitator, in seconds. The default is
- * DEFAULT_FACILITATOR_POLL_INTERVAL. There is a sanity-check minimum of 1.0 s.
- *
- * max_clients=<NUM>
- * How many clients to serve concurrently. The default is
- * DEFAULT_MAX_NUM_PROXY_PAIRS.
- *
- * relay=<HOST>:<PORT>
- * The address of the relay to connect to. The proxy normally receives this
- * information from the facilitator. When this option is used, the facilitator
- * query is not done. The "client" parameter must be given as well.
- *
- * ratelimit=<FLOAT>(<UNIT>)?|off
- * What rate to limit all proxy traffic combined to. The special value "off"
- * disables the limit. The default is DEFAULT_RATE_LIMIT. There is a
- * sanity-check minimum of "10K".
- */
-
-/* WebSocket links.
- *
- * The WebSocket Protocol
- * https://tools.ietf.org/html/rfc6455
- *
- * The WebSocket API
- * http://dev.w3.org/html5/websockets/
- *
- * MDN page with browser compatibility
- * https://developer.mozilla.org/en/WebSockets
- *
- * Implementation tests (including tests of binary messages)
- * http://autobahn.ws/testsuite/reports/clients/index.html
- */
-
-var DEFAULT_FACILITATOR_URL = "https://tor-facilitator.bamsoftware.com/";
-
-var DEFAULT_MAX_NUM_PROXY_PAIRS = 10;
-
-var DEFAULT_FACILITATOR_POLL_INTERVAL = 10.0;
-var MIN_FACILITATOR_POLL_INTERVAL = 1.0;
-
-/* Bytes per second. Set to undefined to disable limit. */
-var DEFAULT_RATE_LIMIT = undefined;
-var MIN_RATE_LIMIT = 10 * 1024;
-var RATE_LIMIT_HISTORY = 5.0;
-
-/* Firefox before version 11.0 uses the name MozWebSocket. Whether the global
-   variable WebSocket is defined indicates whether WebSocket is supported at
-   all. */
-var WebSocket = window.WebSocket || window.MozWebSocket;
-
-var query = parse_query_string(window.location.search.substr(1));
-var debug_div;
-
-if (query.debug) {
-    debug_div = document.createElement("pre");
-    debug_div.className = "debug";
-}
-
-function puts(s) {
-    if (debug_div) {
-        var at_bottom;
-
-        /* http://www.w3.org/TR/cssom-view/#element-scrolling-members */
-        at_bottom = (debug_div.scrollTop + debug_div.clientHeight === debug_div.scrollHeight);
-        debug_div.appendChild(document.createTextNode(s + "\n"));
-        if (at_bottom)
-            debug_div.scrollTop = debug_div.scrollHeight;
-    }
-}
-
-/* Parse a URL query string or application/x-www-form-urlencoded body. The
-   return type is an object mapping string keys to string values. By design,
-   this function doesn't support multiple values for the same named parameter,
-   for example "a=1&a=2&a=3"; the first definition always wins. Returns null on
-   error.
-
-   Always decodes from UTF-8, not any other encoding.
-
-   http://dev.w3.org/html5/spec/Overview.html#url-encoded-form-data */
-function parse_query_string(qs) {
-    var strings;
-    var result;
-
-    result = {};
-    if (qs)
-        strings = qs.split("&");
-    else
-        strings = [];
-    for (var i = 0; i < strings.length; i++) {
-        var string = strings[i];
-        var j, name, value;
-
-        j = string.indexOf("=");
-        if (j === -1) {
-            name = string;
-            value = "";
-        } else {
-            name = string.substr(0, j);
-            value = string.substr(j + 1);
-        }
-        name = decodeURIComponent(name.replace(/\+/g, " "));
-        value = decodeURIComponent(value.replace(/\+/g, " "));
-        if (!(name in result))
-             result[name] = value;
-    }
-
-    return result;
-}
-
-var DEFAULT_PORTS = {
-    http: 80,
-    https: 443
-}
-/* Build an escaped URL string from unescaped components. Only scheme and host
-   are required. See RFC 3986, section 3. */
-function build_url(scheme, host, port, path, params) {
-    var parts = []
-
-    parts.push(encodeURIComponent(scheme));
-    parts.push("://");
-
-    /* If it contains a colon but no square brackets, treat it like an IPv6
-       address. */
-    if (host.match(/:/) && !host.match(/[[\]]/)) {
-        parts.push("[");
-        parts.push(host);
-        parts.push("]");
-    } else {
-        parts.push(encodeURIComponent(host));
-    }
-    if (port !== undefined && port !== DEFAULT_PORTS[scheme]) {
-        parts.push(":");
-        parts.push(encodeURIComponent(port.toString()));
-    }
-
-    if (path !== undefined && path !== "") {
-        if (!path.match(/^\//))
-            path = "/" + path;
-        /* Slash is significant so we must protect it from encodeURIComponent,
-           while still encoding question mark and number sign. RFC 3986, section
-           3.3: "The path is terminated by the first question mark ('?') or
-           number sign ('#') character, or by the end of the URI. ... A path
-           consists of a sequence of path segments separated by a slash ('/')
-           character." */
-        path = path.replace(/[^\/]+/, function(m) {
-            return encodeURIComponent(m);
-        });
-        parts.push(path);
-    }
-
-    if (params !== undefined) {
-        parts.push("?");
-        for (var i = 0; i < params.length; i++) {
-            if (i > 0)
-                parts.push("&");
-            parts.push(encodeURIComponent(params[i][0]) + "=" + encodeURIComponent(params[i][1]));
-        }
-    }
-
-    return parts.join("");
-}
-
-/* Get a query string parameter and return it as a string. Returns default_val
-   if param is not defined in the query string. */
-function get_query_param_string(query, param, default_val) {
-    var val;
-
-    val = query[param];
-    if (val === undefined)
-        return default_val;
-    else
-        return val;
-}
-
-/* Get a query string parameter, or the given default, and parse it as an
-   address spec. Returns null on a parsing error. */
-function get_query_param_addr(query, param, default_val) {
-    var val;
-
-    val = query[param];
-    if (val === undefined)
-        return default_val;
-    else
-        return parse_addr_spec(val);
-}
-
-/* Get an integer from the given movie parameter, or the given default. Returns
-   null on error. */
-function get_query_param_integer(query, param, default_val) {
-    var spec;
-    var val;
-
-    spec = query[param];
-    if (spec === undefined) {
-        return default_val;
-    } else if (!spec.match(/^-?[0-9]+/)) {
-        return null;
-    } else {
-        val = parseInt(spec, 10);
-        if (isNaN(val))
-            return null;
-        else
-            return val;
-    }
-}
-
-/* Get a number from the given movie parameter, or the given default. Returns
-   null on error. */
-function get_query_param_number(query, param, default_val) {
-    var spec;
-    var val;
-
-    spec = query[param];
-    if (spec === undefined) {
-        return default_val;
-    } else {
-        val = Number(spec);
-        if (isNaN(val))
-            return null;
-        else
-            return val;
-    }
-}
-
-/* Get a floating-point number of seconds from a time specification. The only
-   time specification format is a decimal number of seconds. Returns null on
-   error. */
-function get_query_param_timespec(query, param, default_val) {
-    return get_query_param_number(query, param, default_val);
-}
-
-/* Parse a count of bytes. A suffix of "k", "m", or "g" (or uppercase)
-   does what you would think. Returns null on error. */
-function parse_byte_count(spec) {
-    var UNITS = {
-        k: 1024, m: 1024 * 1024, g: 1024 * 1024 * 1024,
-        K: 1024, M: 1024 * 1024, G: 1024 * 1024 * 1024
-    };
-    var count, units;
-    var matches;
-
-    matches = spec.match(/^(\d+(?:\.\d*)?)(\w*)$/);
-    if (matches === null)
-        return null;
-
-    count = Number(matches[1]);
-    if (isNaN(count))
-        return null;
-
-    if (matches[2] === "") {
-        units = 1;
-    } else {
-        units = UNITS[matches[2]];
-        if (units === null)
-            return null;
-    }
-
-    return count * Number(units);
-}
-
-/* Get a count of bytes from a string specification like "100" or "1.3m".
-   Returns null on error. */
-function get_query_param_byte_count(query, param, default_val) {
-    var spec;
-
-    spec = query[param];
-    if (spec === undefined)
-        return default_val;
-    else
-        return parse_byte_count(spec);
-}
-
-/* Parse an address in the form "host:port". Returns an Object with
-   keys "host" (String) and "port" (int). Returns null on error. */
-function parse_addr_spec(spec) {
-    var m, host, port;
-
-    m = null;
-    /* IPv6 syntax. */
-    if (!m)
-        m = spec.match(/^\[([\0-9a-fA-F:.]+)\]:([0-9]+)$/);
-    /* IPv4 syntax. */
-    if (!m)
-        m = spec.match(/^([0-9.]+):([0-9]+)$/);
-    if (!m)
-        return null;
-    host = m[1];
-    port = parseInt(m[2], 10);
-    if (isNaN(port) || port < 0 || port > 65535)
-        return null;
-
-    return { host: host, port: port }
-}
-
-function format_addr(addr) {
-    return addr.host + ":" + addr.port;
-}
-
-/* Does the WebSocket implementation in this browser support binary frames? (RFC
-   6455 section 5.6.) If not, we have to use base64-encoded text frames. It is
-   assumed that the client and relay endpoints always support binary frames. */
-function have_websocket_binary_frames() {
-    var ua, matches;
-
-    ua = window.navigator.userAgent;
-    if (ua === null)
-        return false;
-
-    /* We are cool for Chrome 16 or Safari 6.0. */
-
-    matches = ua.match(/\bchrome\/(\d+)/i);
-    if (matches !== null && Number(matches[1]) >= 16)
-        return true;
-
-    matches = ua.match(/\bversion\/(\d+)/i);
-    if (ua.match(/\bsafari\b/i) && !ua.match(/\bchrome\b/i)
-        && Number(matches[1]) >= 6)
-        return true;
-
-    return false;
-}
-
-function make_websocket(addr) {
-    var url;
-    var ws;
-
-    url = build_url("ws", addr.host, addr.port, "/");
-
-    if (have_websocket_binary_frames())
-        ws = new WebSocket(url);
-    else
-        ws = new WebSocket(url, "base64");
-    /* "User agents can use this as a hint for how to handle incoming binary
-       data: if the attribute is set to 'blob', it is safe to spool it to disk,
-       and if it is set to 'arraybuffer', it is likely more efficient to keep
-       the data in memory." */
-    ws.binaryType = "arraybuffer";
-
-    return ws;
-}
-
-function FlashProxy() {
-    if (query.debug) {
-        this.badge_elem = debug_div;
-    } else {
-        this.badge = new Badge();
-        this.badge.elem.onmouseover = function(event) {
-            this.badge.disable_button.style.display = "block";
-        }.bind(this);
-        this.badge.elem.onmouseout = function(event) {
-            this.badge.disable_button.style.display = "none";
-        }.bind(this);
-        /* Click a button to disable the badge. */
-        this.badge.disable_button.onclick = function(event) {
-            this.disable();
-            this.badge.disable_button.parentNode.removeChild(this.badge.disable_button);
-        }.bind(this);
-        this.badge_elem = this.badge.elem;
-    }
-    this.badge_elem.setAttribute("id", "flashproxy-badge");
-
-    this.proxy_pairs = [];
-
-    this.start = function() {
-        var client_addr;
-        var relay_addr;
-        var rate_limit_bytes;
-
-        this.fac_url = get_query_param_string(query, "facilitator", DEFAULT_FACILITATOR_URL);
-
-        this.max_num_proxy_pairs = get_query_param_integer(query, "max_clients", DEFAULT_MAX_NUM_PROXY_PAIRS);
-        if (this.max_num_proxy_pairs === null || this.max_num_proxy_pairs < 0) {
-            puts("Error: max_clients must be a nonnegative integer.");
-            this.die();
-            return;
-        }
-
-        this.facilitator_poll_interval = get_query_param_timespec(query, "facilitator_poll_interval", DEFAULT_FACILITATOR_POLL_INTERVAL);
-        if (this.facilitator_poll_interval === null || this.facilitator_poll_interval < MIN_FACILITATOR_POLL_INTERVAL) {
-            puts("Error: facilitator_poll_interval must be a nonnegative number at least " + MIN_FACILITATOR_POLL_INTERVAL + ".");
-            this.die();
-            return;
-        }
-
-        if (query["ratelimit"] === "off")
-            rate_limit_bytes = undefined;
-        else
-            rate_limit_bytes = get_query_param_byte_count(query, "ratelimit", DEFAULT_RATE_LIMIT);
-        if (rate_limit_bytes === undefined) {
-            this.rate_limit = new DummyRateLimit();
-        } else if (rate_limit_bytes === null || rate_limit_bytes < MIN_FACILITATOR_POLL_INTERVAL) {
-            puts("Error: ratelimit must be a nonnegative number at least " + MIN_RATE_LIMIT + ".");
-            this.die();
-            return;
-        } else {
-            this.rate_limit = new BucketRateLimit(rate_limit_bytes * RATE_LIMIT_HISTORY, RATE_LIMIT_HISTORY);
-        }
-
-        client_addr = get_query_param_addr(query, "client");
-        relay_addr = get_query_param_addr(query, "relay");
-        if (client_addr !== undefined && relay_addr !== undefined) {
-            this.make_proxy_pair(client_addr, relay_addr);
-        } else if (client_addr !== undefined) {
-            puts("Error: the \"client\" parameter requires \"relay\" also.")
-            this.die();
-            return;
-        } else if (relay_addr !== undefined) {
-            puts("Error: the \"relay\" parameter requires \"client\" also.")
-            this.die();
-            return;
-        } else {
-            this.proxy_main();
-        }
-    };
-
-    this.proxy_main = function() {
-        var xhr;
-
-        if (this.proxy_pairs.length >= this.max_num_proxy_pairs) {
-            setTimeout(this.proxy_main.bind(this), this.facilitator_poll_interval);
-            return;
-        }
-
-        xhr = new XMLHttpRequest();
-        try {
-            xhr.open("GET", this.fac_url);
-        } catch (err) {
-            /* An exception happens here when, for example, NoScript allows the
-               domain on which the proxy badge runs, but not the domain to which
-               it's trying to make the HTTP request. The exception message is
-               like "Component returned failure code: 0x805e0006
-               [nsIXMLHttpRequest.open]" on Firefox. */
-            puts("Facilitator: exception while connecting: " + repr(err.message) + ".");
-            this.die();
-            return;
-        }
-        xhr.responseType = "text";
-        xhr.onreadystatechange = function() {
-            if (xhr.readyState === xhr.DONE) {
-                if (xhr.status === 200)
-                    this.fac_complete(xhr.responseText);
-                else
-                    puts("Facilitator: can't connect: got status " + repr(xhr.status) + " and status text " + repr(xhr.statusText) + ".");
-            }
-        }.bind(this);
-        puts("Facilitator: connecting to " + this.fac_url + ".");
-        xhr.send(null);
-    };
-
-    this.fac_complete = function(text) {
-        var response;
-        var client_addr;
-        var relay_addr;
-
-        setTimeout(this.proxy_main.bind(this), this.facilitator_poll_interval * 1000);
-
-        response = parse_query_string(text);
-
-        if (!response.client) {
-            puts("No clients.");
-            return;
-        }
-        client_addr = parse_addr_spec(response.client);
-        if (client_addr === null) {
-            puts("Error: can't parse client spec " + repr(response.client) + ".");
-            return;
-        }
-        if (!response.relay) {
-            puts("Error: missing relay in response.");
-            return;
-        }
-        relay_addr = parse_addr_spec(response.relay);
-        if (relay_addr === null) {
-            puts("Error: can't parse relay spec " + repr(response.relay) + ".");
-            return;
-        }
-        puts("Facilitator: got client:" + repr(client_addr) + " "
-            + "relay:" + repr(relay_addr) + ".");
-
-        this.make_proxy_pair(client_addr, relay_addr);
-    };
-
-    this.make_proxy_pair = function(client_addr, relay_addr) {
-        var proxy_pair;
-
-        proxy_pair = new ProxyPair(client_addr, relay_addr, this.rate_limit);
-        this.proxy_pairs.push(proxy_pair);
-        proxy_pair.complete_callback = function(event) {
-            puts("Complete.");
-            /* Delete from the list of active proxy pairs. */
-            this.proxy_pairs.splice(this.proxy_pairs.indexOf(proxy_pair), 1);
-            if (this.badge)
-                this.badge.proxy_end();
-        }.bind(this);
-        try {
-            proxy_pair.connect();
-        } catch (err) {
-            puts("ProxyPair: exception while connecting: " + repr(err.message) + ".");
-            this.die();
-            return;
-        }
-
-        if (this.badge)
-            this.badge.proxy_begin();
-    };
-
-    /* Cease all network operations and prevent any future ones. */
-    this.disable = function() {
-        puts("Disabling.");
-        this.proxy_main = function() { };
-        this.make_proxy_pair = function(client_addr, relay_addr) { };
-        while (this.proxy_pairs.length > 0)
-            this.proxy_pairs.pop().close();
-        if (this.badge)
-            this.badge.disable();
-    };
-
-    this.die = function() {
-        puts("Dying.");
-        if (this.badge)
-            this.badge.die();
-    };
-}
-
-/* An instance of a client-relay connection. */
-function ProxyPair(client_addr, relay_addr, rate_limit) {
-    var MAX_BUFFER = 10 * 1024 * 1024;
-
-    function log(s)
-    {
-        puts(s)
-    }
-
-    this.client_addr = client_addr;
-    this.relay_addr = relay_addr;
-    this.rate_limit = rate_limit;
-
-    this.c2r_schedule = [];
-    this.r2c_schedule = [];
-
-    this.running = true;
-    this.flush_timeout_id = null;
-
-    /* This callback function can be overridden by external callers. */
-    this.complete_callback = function() {
-    };
-
-    /* Return a function that shows an error message and closes the other
-       half of a communication pair. */
-    this.make_onerror_callback = function(partner)
-    {
-        return function(event) {
-            var ws = event.target;
-
-            log(ws.label + ": error.");
-            partner.close();
-        }.bind(this);
-    };
-
-    this.onopen_callback = function(event) {
-        var ws = event.target;
-
-        log(ws.label + ": connected.");
-    }.bind(this);
-
-    this.onclose_callback = function(event) {
-        var ws = event.target;
-
-        log(ws.label + ": closed.");
-        this.flush();
-
-        if (this.running && is_closed(this.client_s) && is_closed(this.relay_s)) {
-            this.running = false;
-            this.complete_callback();
-        }
-    }.bind(this);
-
-    this.onmessage_client_to_relay = function(event) {
-        this.c2r_schedule.push(event.data);
-        this.flush();
-    }.bind(this);
-
-    this.onmessage_relay_to_client = function(event) {
-        this.r2c_schedule.push(event.data);
-        this.flush();
-    }.bind(this);
-
-    this.connect = function() {
-        log("Client: connecting.");
-        this.client_s = make_websocket(this.client_addr);
-
-        log("Relay: connecting.");
-        this.relay_s = make_websocket(this.relay_addr);
-
-        this.client_s.label = "Client";
-        this.client_s.onopen = this.onopen_callback;
-        this.client_s.onclose = this.onclose_callback;
-        this.client_s.onerror = this.make_onerror_callback(this.relay_s);
-        this.client_s.onmessage = this.onmessage_client_to_relay;
-
-        this.relay_s.label = "Relay";
-        this.relay_s.onopen = this.onopen_callback;
-        this.relay_s.onclose = this.onclose_callback;
-        this.relay_s.onerror = this.make_onerror_callback(this.client_s);
-        this.relay_s.onmessage = this.onmessage_relay_to_client;
-    };
-
-    function is_open(ws)
-    {
-        return ws.readyState === ws.OPEN;
-    }
-
-    function is_closed(ws)
-    {
-        return ws.readyState === ws.CLOSED;
-    }
-
-    this.close = function() {
-        this.client_s.close();
-        this.relay_s.close();
-    };
-
-    /* Send as much data as the rate limit currently allows. */
-    this.flush = function() {
-        var busy;
-
-        if (this.flush_timeout_id)
-            clearTimeout(this.flush_timeout_id);
-        this.flush_timeout_id = null;
-
-        busy = true;
-        while (busy && !this.rate_limit.is_limited()) {
-            var chunk;
-
-            busy = false;
-            if (is_open(this.client_s) && this.client_s.bufferedAmount < MAX_BUFFER && this.r2c_schedule.length > 0) {
-                chunk = this.r2c_schedule.shift();
-                this.rate_limit.update(chunk.length);
-                this.client_s.send(chunk);
-                busy = true;
-            }
-            if (is_open(this.relay_s) && this.relay_s.bufferedAmount < MAX_BUFFER && this.c2r_schedule.length > 0) {
-                chunk = this.c2r_schedule.shift();
-                this.rate_limit.update(chunk.length);
-                this.relay_s.send(chunk);
-                busy = true;
-            }
-        }
-
-        if (is_closed(this.relay_s) && !is_closed(this.client_s) && this.client_s.bufferedAmount === 0 && this.r2c_schedule.length === 0) {
-            log("Client: closing.");
-            this.client_s.close();
-        }
-        if (is_closed(this.client_s) && !is_closed(this.relay_s) && this.relay_s.bufferedAmount === 0 && this.c2r_schedule.length === 0) {
-            log("Relay: closing.");
-            this.relay_s.close();
-        }
-
-        if (this.r2c_schedule.length > 0 || this.client_s.bufferedAmount > 0
-            || this.c2r_schedule.length > 0 || this.relay_s.bufferedAmount > 0)
-            this.flush_timeout_id = setTimeout(this.flush.bind(this), this.rate_limit.when() * 1000);
-    };
-}
-
-function BucketRateLimit(capacity, time) {
-    this.amount = 0.0;
-    /* capacity / time is the rate we are aiming for. */
-    this.capacity = capacity;
-    this.time = time;
-    this.last_update = new Date();
-
-    this.age = function() {
-        var now;
-        var delta;
-
-        now = new Date();
-        delta = (now - this.last_update) / 1000.0;
-        this.last_update = now;
-
-        this.amount -= delta * this.capacity / this.time;
-        if (this.amount < 0.0)
-            this.amount = 0.0;
-    };
-
-    this.update = function(n) {
-        this.age();
-        this.amount += n;
-
-        return this.amount <= this.capacity;
-    };
-
-    /* How many seconds in the future will the limit expire? */
-    this.when = function() {
-        this.age();
-
-        return (this.amount - this.capacity) / (this.capacity / this.time);
-    }
-
-    this.is_limited = function() {
-        this.age();
-
-        return this.amount > this.capacity;
-    }
-}
-
-/* A rate limiter that never limits. */
-function DummyRateLimit(capacity, time) {
-    this.update = function(n) {
-        return true;
-    };
-
-    this.when = function() {
-        return 0.0;
-    }
-
-    this.is_limited = function() {
-        return false;
-    }
-}
-
-var HTML_ESCAPES = {
-    "&": "amp",
-    "<": "lt",
-    ">": "gt",
-    "'": "apos",
-    "\"": "quot"
-};
-function escape_html(s) {
-    return s.replace(/&<>'"/, function(x) { return HTML_ESCAPES[x] });
-}
-
-/* The usual embedded HTML badge. The "elem" member is a DOM element that can be
-   included elsewhere. */
-function Badge() {
-    /* Number of proxy pairs currently connected. */
-    this.num_proxy_pairs = 0;
-
-    var table, tr, td, div, a, img;
-
-    table = document.createElement("table");
-    tr = document.createElement("tr");
-    table.appendChild(tr);
-    td = document.createElement("td");
-    tr.appendChild(td);
-    a = document.createElement("a");
-    a.setAttribute("href", "http://crypto.stanford.edu/flashproxy/");
-    a.setAttribute("target", "_parent");
-    td.appendChild(a);
-    img = document.createElement("img");
-    img.setAttribute("src", "badge.png");
-    img.setAttribute("alt", "Internet freedom");
-    a.appendChild(img);
-
-    this.elem = table;
-    this.elem.className = "idle";
-
-    a = document.createElement("a");
-    a.setAttribute("href", "#");
-    this.disable_button = document.createElement("div");
-    /* HEAVY MULTIPLICATION X */
-    this.disable_button.innerHTML = "&#x2716;";
-    this.disable_button.className = "disable-button";
-    a.appendChild(this.disable_button);
-    td.appendChild(a);
-
-    this.proxy_begin = function() {
-        this.num_proxy_pairs++;
-        this.elem.className = "active";
-    };
-
-    this.proxy_end = function() {
-        this.num_proxy_pairs--;
-        if (this.num_proxy_pairs <= 0) {
-            this.elem.className = "idle";
-        }
-    }
-
-    this.disable = function() {
-        this.elem.className = "disabled";
-        this.disable_button.style.display = "none";
-    }
-
-    this.die = function() {
-        this.elem.className = "dead";
-        this.disable_button.style.display = "none";
-    }
-}
-
-function quote(s) {
-    return "\"" + s.replace(/([\\\"])/, "\\$1") + "\"";
-}
-
-function maybe_quote(s) {
-    if (!/^[a-zA-Z_]\w*$/.test(s))
-        return quote(s);
-    else
-        return s;
-}
-
-function repr(x) {
-    if (x === null) {
-        return "null";
-    } else if (typeof x === "undefined") {
-        return "undefined";
-    } else if (typeof x === "object") {
-        var elems = [];
-        for (var k in x)
-            elems.push(maybe_quote(k) + ": " + repr(x[k]));
-        return "{ " + elems.join(", ") + " }";
-    } else if (typeof x === "string") {
-        return quote(x);
-    } else {
-        return x.toString();
-    }
-}
-
-/* Are circumstances such that we should self-disable and not be a
-   proxy? We take a best-effort guess as to whether this device runs on
-   a battery or the data transfer might be expensive.
-
-   http://www.zytrax.com/tech/web/mobile_ids.html
-   http://googlewebmastercentral.blogspot.com/2011/03/mo-better-to-also-detect-mobile-user.html
-   http://search.cpan.org/~cmanley/Mobile-UserAgent-1.05/lib/Mobile/UserAgent.pm
-*/
-function flashproxy_should_disable() {
-    var ua;
-
-    ua = window.navigator.userAgent;
-    if (ua !== null) {
-        var UA_LIST = [
-            /\bmobile\b/i,
-            /\bandroid\b/i,
-            /\bopera mobi\b/i,
-        ];
-
-        for (var i = 0; i < UA_LIST.length; i++) {
-            var re = UA_LIST[i];
-
-            if (ua.match(re)) {
-                puts("Disable because User-Agent matches mobile pattern " + re + ".");
-                return true;
-            }
-        }
-    }
-
-    if (ua.match(/\bsafari\b/i) && !ua.match(/\bchrome\b/i)
-        && !ua.match(/\bversion\/[6789]\./i)) {
-        /* Disable before Safari 6.0 because it doesn't have the hybi/RFC type
-           of WebSockets. */
-        puts("Disable because User-Agent is Safari before 6.0.");
-        return true;
-    }
-
-    if (!WebSocket) {
-        /* No WebSocket support. */
-        puts("Disable because of no WebSocket support.");
-        return true;
-    }
-
-    return false;
-}
-
-function flashproxy_badge_insert() {
-    var fp;
-    var e;
-
-    fp = new FlashProxy();
-    if (flashproxy_should_disable())
-        fp.disable();
-
-    /* http://intertwingly.net/blog/2006/11/10/Thats-Not-Write for this trick to
-       insert right after the <script> element in the DOM. */
-    e = document;
-    while (e.lastChild && e.lastChild.nodeType === 1) {
-        e = e.lastChild;
-    }
-    e.parentNode.appendChild(fp.badge_elem);
-
-    return fp;
-}
diff --git a/proxy/README b/proxy/README
new file mode 100644
index 0000000..b9d9003
--- /dev/null
+++ b/proxy/README
@@ -0,0 +1,4 @@
+This directory contains the flash proxy JavaScript proxy program and
+associated HTML and media files. End users don't ahve to do anything
+with these files. They are meant to be installed on a centralized web
+server and then accessed through a browser.
diff --git a/proxy/badge.png b/proxy/badge.png
new file mode 100644
index 0000000..068cf78
Binary files /dev/null and b/proxy/badge.png differ
diff --git a/proxy/badge.xcf b/proxy/badge.xcf
new file mode 100644
index 0000000..d12ff1f
Binary files /dev/null and b/proxy/badge.xcf differ
diff --git a/proxy/embed.html b/proxy/embed.html
new file mode 100644
index 0000000..95821ce
--- /dev/null
+++ b/proxy/embed.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<meta http-equiv="refresh" content="86400">
+<style type="text/css">
+html {
+	width: 100%;
+	height: 100%;
+}
+body {
+	margin: 0;
+	padding: 0;
+	width: 100%;
+	height: 100%;
+}
+#flashproxy-badge {
+	width: 100%;
+	height: 100%;
+	margin: 0;
+	padding: 0;
+	border: 0;
+	border-collapse: collapse;
+	line-height: 0;
+}
+#flashproxy-badge.idle {
+	background-color: #227;
+}
+#flashproxy-badge.active {
+	background-color: #28f;
+}
+#flashproxy-badge.disabled {
+	background-color: #777;
+}
+#flashproxy-badge.dead {
+	background-color: #111;
+}
+#flashproxy-badge td {
+	position: relative;
+	margin: 0;
+	padding: 0;
+	vertical-align: middle;
+	text-align: center;
+}
+#flashproxy-badge td .disable-button {
+	position: absolute;
+	margin: 0;
+	padding: 0;
+	border: 1px solid white;
+	color: white;
+	background-color: #B00;
+	top: 1px;
+	right: 1px;
+	width: 11px;
+	height: 11px;
+	text-align: center;
+	line-height: 11px;
+	font-size: 11px;
+	display: none;
+}
+#flashproxy-badge a img {
+	border: 0;
+}
+#flashproxy-badge.debug {
+	position: absolute;
+	margin: 0;
+	left: 0;
+	top: 0;
+	width: 100%;
+	height: 100%;
+	overflow: auto;
+	color: #4c4;
+	background-color: #021;
+	line-height: inherit;
+}
+</style>
+</head>
+<body>
+<script type="text/javascript" src="flashproxy.js"></script>
+<script type="text/javascript">
+flashproxy_badge_insert().start();
+</script>
+<noscript>
+<table id="flashproxy-badge" class="disabled"><tr><td><a href="//crypto.stanford.edu/flashproxy/" target="_parent"><img src="badge.png" alt="Internet freedom"></a></td></tr></table>
+</noscript>
+</body>
+</html>
diff --git a/proxy/flashproxy-test.js b/proxy/flashproxy-test.js
new file mode 100755
index 0000000..2dc185a
--- /dev/null
+++ b/proxy/flashproxy-test.js
@@ -0,0 +1,239 @@
+#!/usr/bin/js
+
+/* To run this test program, install the Rhino JavaScript interpreter
+   (apt-get install rhino). */
+
+var VERBOSE = false;
+if ("-v" in arguments)
+    VERBOSE = true;
+
+var num_tests = 0;
+var num_failed = 0;
+
+var window = {location: {search: "?"}};
+
+load("flashproxy.js");
+
+function objects_equal(a, b)
+{
+    if ((a === null) != (b === null))
+        return false;
+    if (typeof a != typeof b)
+        return false;
+    if (typeof a != "object")
+        return a == b;
+
+    for (var k in a) {
+        if (!objects_equal(a[k], b[k]))
+            return false;
+    }
+    for (var k in b) {
+        if (!objects_equal(a[k], b[k]))
+            return false;
+    }
+
+    return true;
+}
+
+var top = true;
+function announce(test_name)
+{
+    if (VERBOSE) {
+        if (!top)
+            print();
+        print(test_name);
+    }
+    top = false;
+}
+
+function pass(test)
+{
+    num_tests++;
+    if (VERBOSE)
+        print("PASS " + repr(test));
+}
+
+function fail(test, expected, actual)
+{
+    num_tests++;
+    num_failed++;
+    print("FAIL " + repr(test) + "  expected: " + repr(expected) + "  actual: " + repr(actual));
+}
+
+function test_build_url()
+{
+    var TESTS = [
+        { args: ["http", "example.com"],
+          expected: "http://example.com" },
+        { args: ["http", "example.com", 80],
+          expected: "http://example.com" },
+        { args: ["http", "example.com", 81],
+          expected: "http://example.com:81" },
+        { args: ["https", "example.com", 443],
+          expected: "https://example.com" },
+        { args: ["https", "example.com", 444],
+          expected: "https://example.com:444" },
+        { args: ["http", "example.com", 80, "/"],
+          expected: "http://example.com/" },
+        { args: ["http", "example.com", 80, "/test?k=%#v"],
+          expected: "http://example.com/test%3Fk%3D%25%23v" },
+        { args: ["http", "example.com", 80, "/test", []],
+          expected: "http://example.com/test?" },
+        { args: ["http", "example.com", 80, "/test", [["k", "%#v"]]],
+          expected: "http://example.com/test?k=%25%23v" },
+        { args: ["http", "example.com", 80, "/test", [["a", "b"], ["c", "d"]]],
+          expected: "http://example.com/test?a=b&c=d" },
+        { args: ["http", "1.2.3.4"],
+          expected: "http://1.2.3.4" },
+        { args: ["http", "1:2::3:4"],
+          expected: "http://[1:2::3:4]" },
+        { args: ["http", "bog][us"],
+          expected: "http://bog%5D%5Bus" },
+        { args: ["http", "bog:u]s"],
+          expected: "http://bog%3Au%5Ds" },
+    ];
+
+    announce("test_build_url");
+    for (var i = 0; i < TESTS.length; i++) {
+        var test = TESTS[i];
+        var actual;
+
+        actual = build_url.apply(undefined, test.args);
+        if (objects_equal(actual, test.expected))
+            pass(test.args);
+        else
+            fail(test.args, test.expected, actual);
+    }
+}
+
+function test_parse_query_string()
+{
+    var TESTS = [
+        { qs: "",
+          expected: { } },
+        { qs: "a=b",
+          expected: { a: "b" } },
+        { qs: "a=b=c",
+          expected: { a: "b=c" } },
+        { qs: "a=b&c=d",
+          expected: { a: "b", c: "d" } },
+        { qs: "client=&relay=1.2.3.4%3A9001",
+          expected: { client: "", relay: "1.2.3.4:9001" } },
+        { qs: "a=b%26c=d",
+          expected: { a: "b&c=d" } },
+        { qs: "a%3db=d",
+          expected: { "a=b": "d" } },
+        { qs: "a=b+c%20d",
+          expected: { "a": "b c d" } },
+        { qs: "a=b+c%2bd",
+          expected: { "a": "b c+d" } },
+        { qs: "a+b=c",
+          expected: { "a b": "c" } },
+        { qs: "a=b+c+d",
+          expected: { a: "b c d" } },
+        /* First appearance wins. */
+        { qs: "a=b&c=d&a=e",
+          expected: { a: "b", c: "d" } },
+        { qs: "a",
+          expected: { a: "" } },
+        { qs: "=b",
+          expected: { "": "b" } },
+        { qs: "&a=b",
+          expected: { "": "", a: "b" } },
+        { qs: "a=b&",
+          expected: { "": "", a: "b" } },
+        { qs: "a=b&&c=d",
+          expected: { "": "", a: "b", c: "d" } },
+    ];
+
+    announce("test_parse_query_string");
+    for (var i = 0; i < TESTS.length; i++) {
+        var test = TESTS[i];
+        var actual;
+
+        actual = parse_query_string(test.qs);
+        if (objects_equal(actual, test.expected))
+            pass(test.qs);
+        else
+            fail(test.qs, test.expected, actual);
+    }
+}
+
+function test_parse_addr_spec()
+{
+    var TESTS = [
+        { spec: "",
+          expected: null },
+        { spec: "3.3.3.3:4444",
+          expected: { host: "3.3.3.3", port: 4444 } },
+        { spec: "3.3.3.3",
+          expected: null },
+        { spec: "3.3.3.3:0x1111",
+          expected: null },
+        { spec: "3.3.3.3:-4444",
+          expected: null },
+        { spec: "3.3.3.3:65536",
+          expected: null },
+        { spec: "[1:2::a:f]:4444",
+          expected: { host: "1:2::a:f", port: 4444 } },
+        { spec: "[1:2::a:f]",
+          expected: null },
+        { spec: "[1:2::a:f]:0x1111",
+          expected: null },
+        { spec: "[1:2::a:f]:-4444",
+          expected: null },
+        { spec: "[1:2::a:f]:65536",
+          expected: null },
+        { spec: "[1:2::ffff:1.2.3.4]:4444",
+          expected: { host: "1:2::ffff:1.2.3.4", port: 4444 } },
+    ];
+
+    announce("test_parse_addr_spec");
+    for (var i = 0; i < TESTS.length; i++) {
+        var test = TESTS[i];
+        var actual;
+
+        actual = parse_addr_spec(test.spec);
+        if (objects_equal(actual, test.expected))
+            pass(test.spec);
+        else
+            fail(test.spec, test.expected, actual);
+    }
+}
+
+function test_get_query_param_addr()
+{
+    var DEFAULT = { host: "1.1.1.1", port: 2222 };
+    var TESTS = [
+        { query: { },
+          expected: DEFAULT },
+        { query: { addr: "3.3.3.3:4444" },
+          expected: { host: "3.3.3.3", port: 4444 } },
+        { query: { x: "3.3.3.3:4444" },
+          expected: DEFAULT },
+        { query: { addr: "---" },
+          expected: null },
+    ];
+
+    announce("test_get_query_param_addr");
+    for (var i = 0; i < TESTS.length; i++) {
+        var test = TESTS[i];
+        var actual;
+
+        actual = get_query_param_addr(test.query, "addr", DEFAULT);
+        if (objects_equal(actual, test.expected))
+            pass(test.query);
+        else
+            fail(test.query, test.expected, actual);
+    }
+}
+
+test_build_url();
+test_parse_query_string();
+test_parse_addr_spec();
+test_get_query_param_addr();
+
+if (num_failed == 0)
+    quit(0);
+else
+    quit(1);
diff --git a/proxy/flashproxy.js b/proxy/flashproxy.js
new file mode 100644
index 0000000..a2e8863
--- /dev/null
+++ b/proxy/flashproxy.js
@@ -0,0 +1,898 @@
+/* Query string parameters. These change how the program runs from the outside.
+ * For example:
+ *   http://www.example.com/embed.html?facilitator=127.0.0.1:9002&debug=1
+ *
+ * client=<HOST>:<PORT>
+ * The address of the client to connect to. The proxy normally receives this
+ * information from the facilitator. When this option is used, the facilitator
+ * query is not done. The "relay" parameter must be given as well.
+ *
+ * debug=1
+ * If set (to any value), show verbose terminal-like output instead of the
+ * badge.
+ *
+ * facilitator=https://host:port/
+ * The URL of the facilitator CGI script. By default it is
+ * DEFAULT_FACILITATOR_URL.
+ *
+ * facilitator_poll_interval=<FLOAT>
+ * How often to poll the facilitator, in seconds. The default is
+ * DEFAULT_FACILITATOR_POLL_INTERVAL. There is a sanity-check minimum of 1.0 s.
+ *
+ * max_clients=<NUM>
+ * How many clients to serve concurrently. The default is
+ * DEFAULT_MAX_NUM_PROXY_PAIRS.
+ *
+ * relay=<HOST>:<PORT>
+ * The address of the relay to connect to. The proxy normally receives this
+ * information from the facilitator. When this option is used, the facilitator
+ * query is not done. The "client" parameter must be given as well.
+ *
+ * ratelimit=<FLOAT>(<UNIT>)?|off
+ * What rate to limit all proxy traffic combined to. The special value "off"
+ * disables the limit. The default is DEFAULT_RATE_LIMIT. There is a
+ * sanity-check minimum of "10K".
+ */
+
+/* WebSocket links.
+ *
+ * The WebSocket Protocol
+ * https://tools.ietf.org/html/rfc6455
+ *
+ * The WebSocket API
+ * http://dev.w3.org/html5/websockets/
+ *
+ * MDN page with browser compatibility
+ * https://developer.mozilla.org/en/WebSockets
+ *
+ * Implementation tests (including tests of binary messages)
+ * http://autobahn.ws/testsuite/reports/clients/index.html
+ */
+
+var DEFAULT_FACILITATOR_URL = "https://tor-facilitator.bamsoftware.com/";
+
+var DEFAULT_MAX_NUM_PROXY_PAIRS = 10;
+
+var DEFAULT_FACILITATOR_POLL_INTERVAL = 10.0;
+var MIN_FACILITATOR_POLL_INTERVAL = 1.0;
+
+/* Bytes per second. Set to undefined to disable limit. */
+var DEFAULT_RATE_LIMIT = undefined;
+var MIN_RATE_LIMIT = 10 * 1024;
+var RATE_LIMIT_HISTORY = 5.0;
+
+/* Firefox before version 11.0 uses the name MozWebSocket. Whether the global
+   variable WebSocket is defined indicates whether WebSocket is supported at
+   all. */
+var WebSocket = window.WebSocket || window.MozWebSocket;
+
+var query = parse_query_string(window.location.search.substr(1));
+var debug_div;
+
+if (query.debug) {
+    debug_div = document.createElement("pre");
+    debug_div.className = "debug";
+}
+
+function puts(s) {
+    if (debug_div) {
+        var at_bottom;
+
+        /* http://www.w3.org/TR/cssom-view/#element-scrolling-members */
+        at_bottom = (debug_div.scrollTop + debug_div.clientHeight === debug_div.scrollHeight);
+        debug_div.appendChild(document.createTextNode(s + "\n"));
+        if (at_bottom)
+            debug_div.scrollTop = debug_div.scrollHeight;
+    }
+}
+
+/* Parse a URL query string or application/x-www-form-urlencoded body. The
+   return type is an object mapping string keys to string values. By design,
+   this function doesn't support multiple values for the same named parameter,
+   for example "a=1&a=2&a=3"; the first definition always wins. Returns null on
+   error.
+
+   Always decodes from UTF-8, not any other encoding.
+
+   http://dev.w3.org/html5/spec/Overview.html#url-encoded-form-data */
+function parse_query_string(qs) {
+    var strings;
+    var result;
+
+    result = {};
+    if (qs)
+        strings = qs.split("&");
+    else
+        strings = [];
+    for (var i = 0; i < strings.length; i++) {
+        var string = strings[i];
+        var j, name, value;
+
+        j = string.indexOf("=");
+        if (j === -1) {
+            name = string;
+            value = "";
+        } else {
+            name = string.substr(0, j);
+            value = string.substr(j + 1);
+        }
+        name = decodeURIComponent(name.replace(/\+/g, " "));
+        value = decodeURIComponent(value.replace(/\+/g, " "));
+        if (!(name in result))
+             result[name] = value;
+    }
+
+    return result;
+}
+
+var DEFAULT_PORTS = {
+    http: 80,
+    https: 443
+}
+/* Build an escaped URL string from unescaped components. Only scheme and host
+   are required. See RFC 3986, section 3. */
+function build_url(scheme, host, port, path, params) {
+    var parts = []
+
+    parts.push(encodeURIComponent(scheme));
+    parts.push("://");
+
+    /* If it contains a colon but no square brackets, treat it like an IPv6
+       address. */
+    if (host.match(/:/) && !host.match(/[[\]]/)) {
+        parts.push("[");
+        parts.push(host);
+        parts.push("]");
+    } else {
+        parts.push(encodeURIComponent(host));
+    }
+    if (port !== undefined && port !== DEFAULT_PORTS[scheme]) {
+        parts.push(":");
+        parts.push(encodeURIComponent(port.toString()));
+    }
+
+    if (path !== undefined && path !== "") {
+        if (!path.match(/^\//))
+            path = "/" + path;
+        /* Slash is significant so we must protect it from encodeURIComponent,
+           while still encoding question mark and number sign. RFC 3986, section
+           3.3: "The path is terminated by the first question mark ('?') or
+           number sign ('#') character, or by the end of the URI. ... A path
+           consists of a sequence of path segments separated by a slash ('/')
+           character." */
+        path = path.replace(/[^\/]+/, function(m) {
+            return encodeURIComponent(m);
+        });
+        parts.push(path);
+    }
+
+    if (params !== undefined) {
+        parts.push("?");
+        for (var i = 0; i < params.length; i++) {
+            if (i > 0)
+                parts.push("&");
+            parts.push(encodeURIComponent(params[i][0]) + "=" + encodeURIComponent(params[i][1]));
+        }
+    }
+
+    return parts.join("");
+}
+
+/* Get a query string parameter and return it as a string. Returns default_val
+   if param is not defined in the query string. */
+function get_query_param_string(query, param, default_val) {
+    var val;
+
+    val = query[param];
+    if (val === undefined)
+        return default_val;
+    else
+        return val;
+}
+
+/* Get a query string parameter, or the given default, and parse it as an
+   address spec. Returns null on a parsing error. */
+function get_query_param_addr(query, param, default_val) {
+    var val;
+
+    val = query[param];
+    if (val === undefined)
+        return default_val;
+    else
+        return parse_addr_spec(val);
+}
+
+/* Get an integer from the given movie parameter, or the given default. Returns
+   null on error. */
+function get_query_param_integer(query, param, default_val) {
+    var spec;
+    var val;
+
+    spec = query[param];
+    if (spec === undefined) {
+        return default_val;
+    } else if (!spec.match(/^-?[0-9]+/)) {
+        return null;
+    } else {
+        val = parseInt(spec, 10);
+        if (isNaN(val))
+            return null;
+        else
+            return val;
+    }
+}
+
+/* Get a number from the given movie parameter, or the given default. Returns
+   null on error. */
+function get_query_param_number(query, param, default_val) {
+    var spec;
+    var val;
+
+    spec = query[param];
+    if (spec === undefined) {
+        return default_val;
+    } else {
+        val = Number(spec);
+        if (isNaN(val))
+            return null;
+        else
+            return val;
+    }
+}
+
+/* Get a floating-point number of seconds from a time specification. The only
+   time specification format is a decimal number of seconds. Returns null on
+   error. */
+function get_query_param_timespec(query, param, default_val) {
+    return get_query_param_number(query, param, default_val);
+}
+
+/* Parse a count of bytes. A suffix of "k", "m", or "g" (or uppercase)
+   does what you would think. Returns null on error. */
+function parse_byte_count(spec) {
+    var UNITS = {
+        k: 1024, m: 1024 * 1024, g: 1024 * 1024 * 1024,
+        K: 1024, M: 1024 * 1024, G: 1024 * 1024 * 1024
+    };
+    var count, units;
+    var matches;
+
+    matches = spec.match(/^(\d+(?:\.\d*)?)(\w*)$/);
+    if (matches === null)
+        return null;
+
+    count = Number(matches[1]);
+    if (isNaN(count))
+        return null;
+
+    if (matches[2] === "") {
+        units = 1;
+    } else {
+        units = UNITS[matches[2]];
+        if (units === null)
+            return null;
+    }
+
+    return count * Number(units);
+}
+
+/* Get a count of bytes from a string specification like "100" or "1.3m".
+   Returns null on error. */
+function get_query_param_byte_count(query, param, default_val) {
+    var spec;
+
+    spec = query[param];
+    if (spec === undefined)
+        return default_val;
+    else
+        return parse_byte_count(spec);
+}
+
+/* Parse an address in the form "host:port". Returns an Object with
+   keys "host" (String) and "port" (int). Returns null on error. */
+function parse_addr_spec(spec) {
+    var m, host, port;
+
+    m = null;
+    /* IPv6 syntax. */
+    if (!m)
+        m = spec.match(/^\[([\0-9a-fA-F:.]+)\]:([0-9]+)$/);
+    /* IPv4 syntax. */
+    if (!m)
+        m = spec.match(/^([0-9.]+):([0-9]+)$/);
+    if (!m)
+        return null;
+    host = m[1];
+    port = parseInt(m[2], 10);
+    if (isNaN(port) || port < 0 || port > 65535)
+        return null;
+
+    return { host: host, port: port }
+}
+
+function format_addr(addr) {
+    return addr.host + ":" + addr.port;
+}
+
+/* Does the WebSocket implementation in this browser support binary frames? (RFC
+   6455 section 5.6.) If not, we have to use base64-encoded text frames. It is
+   assumed that the client and relay endpoints always support binary frames. */
+function have_websocket_binary_frames() {
+    var ua, matches;
+
+    ua = window.navigator.userAgent;
+    if (ua === null)
+        return false;
+
+    /* We are cool for Chrome 16 or Safari 6.0. */
+
+    matches = ua.match(/\bchrome\/(\d+)/i);
+    if (matches !== null && Number(matches[1]) >= 16)
+        return true;
+
+    matches = ua.match(/\bversion\/(\d+)/i);
+    if (ua.match(/\bsafari\b/i) && !ua.match(/\bchrome\b/i)
+        && Number(matches[1]) >= 6)
+        return true;
+
+    return false;
+}
+
+function make_websocket(addr) {
+    var url;
+    var ws;
+
+    url = build_url("ws", addr.host, addr.port, "/");
+
+    if (have_websocket_binary_frames())
+        ws = new WebSocket(url);
+    else
+        ws = new WebSocket(url, "base64");
+    /* "User agents can use this as a hint for how to handle incoming binary
+       data: if the attribute is set to 'blob', it is safe to spool it to disk,
+       and if it is set to 'arraybuffer', it is likely more efficient to keep
+       the data in memory." */
+    ws.binaryType = "arraybuffer";
+
+    return ws;
+}
+
+function FlashProxy() {
+    if (query.debug) {
+        this.badge_elem = debug_div;
+    } else {
+        this.badge = new Badge();
+        this.badge.elem.onmouseover = function(event) {
+            this.badge.disable_button.style.display = "block";
+        }.bind(this);
+        this.badge.elem.onmouseout = function(event) {
+            this.badge.disable_button.style.display = "none";
+        }.bind(this);
+        /* Click a button to disable the badge. */
+        this.badge.disable_button.onclick = function(event) {
+            this.disable();
+            this.badge.disable_button.parentNode.removeChild(this.badge.disable_button);
+        }.bind(this);
+        this.badge_elem = this.badge.elem;
+    }
+    this.badge_elem.setAttribute("id", "flashproxy-badge");
+
+    this.proxy_pairs = [];
+
+    this.start = function() {
+        var client_addr;
+        var relay_addr;
+        var rate_limit_bytes;
+
+        this.fac_url = get_query_param_string(query, "facilitator", DEFAULT_FACILITATOR_URL);
+
+        this.max_num_proxy_pairs = get_query_param_integer(query, "max_clients", DEFAULT_MAX_NUM_PROXY_PAIRS);
+        if (this.max_num_proxy_pairs === null || this.max_num_proxy_pairs < 0) {
+            puts("Error: max_clients must be a nonnegative integer.");
+            this.die();
+            return;
+        }
+
+        this.facilitator_poll_interval = get_query_param_timespec(query, "facilitator_poll_interval", DEFAULT_FACILITATOR_POLL_INTERVAL);
+        if (this.facilitator_poll_interval === null || this.facilitator_poll_interval < MIN_FACILITATOR_POLL_INTERVAL) {
+            puts("Error: facilitator_poll_interval must be a nonnegative number at least " + MIN_FACILITATOR_POLL_INTERVAL + ".");
+            this.die();
+            return;
+        }
+
+        if (query["ratelimit"] === "off")
+            rate_limit_bytes = undefined;
+        else
+            rate_limit_bytes = get_query_param_byte_count(query, "ratelimit", DEFAULT_RATE_LIMIT);
+        if (rate_limit_bytes === undefined) {
+            this.rate_limit = new DummyRateLimit();
+        } else if (rate_limit_bytes === null || rate_limit_bytes < MIN_FACILITATOR_POLL_INTERVAL) {
+            puts("Error: ratelimit must be a nonnegative number at least " + MIN_RATE_LIMIT + ".");
+            this.die();
+            return;
+        } else {
+            this.rate_limit = new BucketRateLimit(rate_limit_bytes * RATE_LIMIT_HISTORY, RATE_LIMIT_HISTORY);
+        }
+
+        client_addr = get_query_param_addr(query, "client");
+        relay_addr = get_query_param_addr(query, "relay");
+        if (client_addr !== undefined && relay_addr !== undefined) {
+            this.make_proxy_pair(client_addr, relay_addr);
+        } else if (client_addr !== undefined) {
+            puts("Error: the \"client\" parameter requires \"relay\" also.")
+            this.die();
+            return;
+        } else if (relay_addr !== undefined) {
+            puts("Error: the \"relay\" parameter requires \"client\" also.")
+            this.die();
+            return;
+        } else {
+            this.proxy_main();
+        }
+    };
+
+    this.proxy_main = function() {
+        var xhr;
+
+        if (this.proxy_pairs.length >= this.max_num_proxy_pairs) {
+            setTimeout(this.proxy_main.bind(this), this.facilitator_poll_interval);
+            return;
+        }
+
+        xhr = new XMLHttpRequest();
+        try {
+            xhr.open("GET", this.fac_url);
+        } catch (err) {
+            /* An exception happens here when, for example, NoScript allows the
+               domain on which the proxy badge runs, but not the domain to which
+               it's trying to make the HTTP request. The exception message is
+               like "Component returned failure code: 0x805e0006
+               [nsIXMLHttpRequest.open]" on Firefox. */
+            puts("Facilitator: exception while connecting: " + repr(err.message) + ".");
+            this.die();
+            return;
+        }
+        xhr.responseType = "text";
+        xhr.onreadystatechange = function() {
+            if (xhr.readyState === xhr.DONE) {
+                if (xhr.status === 200)
+                    this.fac_complete(xhr.responseText);
+                else
+                    puts("Facilitator: can't connect: got status " + repr(xhr.status) + " and status text " + repr(xhr.statusText) + ".");
+            }
+        }.bind(this);
+        puts("Facilitator: connecting to " + this.fac_url + ".");
+        xhr.send(null);
+    };
+
+    this.fac_complete = function(text) {
+        var response;
+        var client_addr;
+        var relay_addr;
+
+        setTimeout(this.proxy_main.bind(this), this.facilitator_poll_interval * 1000);
+
+        response = parse_query_string(text);
+
+        if (!response.client) {
+            puts("No clients.");
+            return;
+        }
+        client_addr = parse_addr_spec(response.client);
+        if (client_addr === null) {
+            puts("Error: can't parse client spec " + repr(response.client) + ".");
+            return;
+        }
+        if (!response.relay) {
+            puts("Error: missing relay in response.");
+            return;
+        }
+        relay_addr = parse_addr_spec(response.relay);
+        if (relay_addr === null) {
+            puts("Error: can't parse relay spec " + repr(response.relay) + ".");
+            return;
+        }
+        puts("Facilitator: got client:" + repr(client_addr) + " "
+            + "relay:" + repr(relay_addr) + ".");
+
+        this.make_proxy_pair(client_addr, relay_addr);
+    };
+
+    this.make_proxy_pair = function(client_addr, relay_addr) {
+        var proxy_pair;
+
+        proxy_pair = new ProxyPair(client_addr, relay_addr, this.rate_limit);
+        this.proxy_pairs.push(proxy_pair);
+        proxy_pair.complete_callback = function(event) {
+            puts("Complete.");
+            /* Delete from the list of active proxy pairs. */
+            this.proxy_pairs.splice(this.proxy_pairs.indexOf(proxy_pair), 1);
+            if (this.badge)
+                this.badge.proxy_end();
+        }.bind(this);
+        try {
+            proxy_pair.connect();
+        } catch (err) {
+            puts("ProxyPair: exception while connecting: " + repr(err.message) + ".");
+            this.die();
+            return;
+        }
+
+        if (this.badge)
+            this.badge.proxy_begin();
+    };
+
+    /* Cease all network operations and prevent any future ones. */
+    this.disable = function() {
+        puts("Disabling.");
+        this.proxy_main = function() { };
+        this.make_proxy_pair = function(client_addr, relay_addr) { };
+        while (this.proxy_pairs.length > 0)
+            this.proxy_pairs.pop().close();
+        if (this.badge)
+            this.badge.disable();
+    };
+
+    this.die = function() {
+        puts("Dying.");
+        if (this.badge)
+            this.badge.die();
+    };
+}
+
+/* An instance of a client-relay connection. */
+function ProxyPair(client_addr, relay_addr, rate_limit) {
+    var MAX_BUFFER = 10 * 1024 * 1024;
+
+    function log(s)
+    {
+        puts(s)
+    }
+
+    this.client_addr = client_addr;
+    this.relay_addr = relay_addr;
+    this.rate_limit = rate_limit;
+
+    this.c2r_schedule = [];
+    this.r2c_schedule = [];
+
+    this.running = true;
+    this.flush_timeout_id = null;
+
+    /* This callback function can be overridden by external callers. */
+    this.complete_callback = function() {
+    };
+
+    /* Return a function that shows an error message and closes the other
+       half of a communication pair. */
+    this.make_onerror_callback = function(partner)
+    {
+        return function(event) {
+            var ws = event.target;
+
+            log(ws.label + ": error.");
+            partner.close();
+        }.bind(this);
+    };
+
+    this.onopen_callback = function(event) {
+        var ws = event.target;
+
+        log(ws.label + ": connected.");
+    }.bind(this);
+
+    this.onclose_callback = function(event) {
+        var ws = event.target;
+
+        log(ws.label + ": closed.");
+        this.flush();
+
+        if (this.running && is_closed(this.client_s) && is_closed(this.relay_s)) {
+            this.running = false;
+            this.complete_callback();
+        }
+    }.bind(this);
+
+    this.onmessage_client_to_relay = function(event) {
+        this.c2r_schedule.push(event.data);
+        this.flush();
+    }.bind(this);
+
+    this.onmessage_relay_to_client = function(event) {
+        this.r2c_schedule.push(event.data);
+        this.flush();
+    }.bind(this);
+
+    this.connect = function() {
+        log("Client: connecting.");
+        this.client_s = make_websocket(this.client_addr);
+
+        log("Relay: connecting.");
+        this.relay_s = make_websocket(this.relay_addr);
+
+        this.client_s.label = "Client";
+        this.client_s.onopen = this.onopen_callback;
+        this.client_s.onclose = this.onclose_callback;
+        this.client_s.onerror = this.make_onerror_callback(this.relay_s);
+        this.client_s.onmessage = this.onmessage_client_to_relay;
+
+        this.relay_s.label = "Relay";
+        this.relay_s.onopen = this.onopen_callback;
+        this.relay_s.onclose = this.onclose_callback;
+        this.relay_s.onerror = this.make_onerror_callback(this.client_s);
+        this.relay_s.onmessage = this.onmessage_relay_to_client;
+    };
+
+    function is_open(ws)
+    {
+        return ws.readyState === ws.OPEN;
+    }
+
+    function is_closed(ws)
+    {
+        return ws.readyState === ws.CLOSED;
+    }
+
+    this.close = function() {
+        this.client_s.close();
+        this.relay_s.close();
+    };
+
+    /* Send as much data as the rate limit currently allows. */
+    this.flush = function() {
+        var busy;
+
+        if (this.flush_timeout_id)
+            clearTimeout(this.flush_timeout_id);
+        this.flush_timeout_id = null;
+
+        busy = true;
+        while (busy && !this.rate_limit.is_limited()) {
+            var chunk;
+
+            busy = false;
+            if (is_open(this.client_s) && this.client_s.bufferedAmount < MAX_BUFFER && this.r2c_schedule.length > 0) {
+                chunk = this.r2c_schedule.shift();
+                this.rate_limit.update(chunk.length);
+                this.client_s.send(chunk);
+                busy = true;
+            }
+            if (is_open(this.relay_s) && this.relay_s.bufferedAmount < MAX_BUFFER && this.c2r_schedule.length > 0) {
+                chunk = this.c2r_schedule.shift();
+                this.rate_limit.update(chunk.length);
+                this.relay_s.send(chunk);
+                busy = true;
+            }
+        }
+
+        if (is_closed(this.relay_s) && !is_closed(this.client_s) && this.client_s.bufferedAmount === 0 && this.r2c_schedule.length === 0) {
+            log("Client: closing.");
+            this.client_s.close();
+        }
+        if (is_closed(this.client_s) && !is_closed(this.relay_s) && this.relay_s.bufferedAmount === 0 && this.c2r_schedule.length === 0) {
+            log("Relay: closing.");
+            this.relay_s.close();
+        }
+
+        if (this.r2c_schedule.length > 0 || this.client_s.bufferedAmount > 0
+            || this.c2r_schedule.length > 0 || this.relay_s.bufferedAmount > 0)
+            this.flush_timeout_id = setTimeout(this.flush.bind(this), this.rate_limit.when() * 1000);
+    };
+}
+
+function BucketRateLimit(capacity, time) {
+    this.amount = 0.0;
+    /* capacity / time is the rate we are aiming for. */
+    this.capacity = capacity;
+    this.time = time;
+    this.last_update = new Date();
+
+    this.age = function() {
+        var now;
+        var delta;
+
+        now = new Date();
+        delta = (now - this.last_update) / 1000.0;
+        this.last_update = now;
+
+        this.amount -= delta * this.capacity / this.time;
+        if (this.amount < 0.0)
+            this.amount = 0.0;
+    };
+
+    this.update = function(n) {
+        this.age();
+        this.amount += n;
+
+        return this.amount <= this.capacity;
+    };
+
+    /* How many seconds in the future will the limit expire? */
+    this.when = function() {
+        this.age();
+
+        return (this.amount - this.capacity) / (this.capacity / this.time);
+    }
+
+    this.is_limited = function() {
+        this.age();
+
+        return this.amount > this.capacity;
+    }
+}
+
+/* A rate limiter that never limits. */
+function DummyRateLimit(capacity, time) {
+    this.update = function(n) {
+        return true;
+    };
+
+    this.when = function() {
+        return 0.0;
+    }
+
+    this.is_limited = function() {
+        return false;
+    }
+}
+
+var HTML_ESCAPES = {
+    "&": "amp",
+    "<": "lt",
+    ">": "gt",
+    "'": "apos",
+    "\"": "quot"
+};
+function escape_html(s) {
+    return s.replace(/&<>'"/, function(x) { return HTML_ESCAPES[x] });
+}
+
+/* The usual embedded HTML badge. The "elem" member is a DOM element that can be
+   included elsewhere. */
+function Badge() {
+    /* Number of proxy pairs currently connected. */
+    this.num_proxy_pairs = 0;
+
+    var table, tr, td, div, a, img;
+
+    table = document.createElement("table");
+    tr = document.createElement("tr");
+    table.appendChild(tr);
+    td = document.createElement("td");
+    tr.appendChild(td);
+    a = document.createElement("a");
+    a.setAttribute("href", "http://crypto.stanford.edu/flashproxy/");
+    a.setAttribute("target", "_parent");
+    td.appendChild(a);
+    img = document.createElement("img");
+    img.setAttribute("src", "badge.png");
+    img.setAttribute("alt", "Internet freedom");
+    a.appendChild(img);
+
+    this.elem = table;
+    this.elem.className = "idle";
+
+    a = document.createElement("a");
+    a.setAttribute("href", "#");
+    this.disable_button = document.createElement("div");
+    /* HEAVY MULTIPLICATION X */
+    this.disable_button.innerHTML = "&#x2716;";
+    this.disable_button.className = "disable-button";
+    a.appendChild(this.disable_button);
+    td.appendChild(a);
+
+    this.proxy_begin = function() {
+        this.num_proxy_pairs++;
+        this.elem.className = "active";
+    };
+
+    this.proxy_end = function() {
+        this.num_proxy_pairs--;
+        if (this.num_proxy_pairs <= 0) {
+            this.elem.className = "idle";
+        }
+    }
+
+    this.disable = function() {
+        this.elem.className = "disabled";
+        this.disable_button.style.display = "none";
+    }
+
+    this.die = function() {
+        this.elem.className = "dead";
+        this.disable_button.style.display = "none";
+    }
+}
+
+function quote(s) {
+    return "\"" + s.replace(/([\\\"])/, "\\$1") + "\"";
+}
+
+function maybe_quote(s) {
+    if (!/^[a-zA-Z_]\w*$/.test(s))
+        return quote(s);
+    else
+        return s;
+}
+
+function repr(x) {
+    if (x === null) {
+        return "null";
+    } else if (typeof x === "undefined") {
+        return "undefined";
+    } else if (typeof x === "object") {
+        var elems = [];
+        for (var k in x)
+            elems.push(maybe_quote(k) + ": " + repr(x[k]));
+        return "{ " + elems.join(", ") + " }";
+    } else if (typeof x === "string") {
+        return quote(x);
+    } else {
+        return x.toString();
+    }
+}
+
+/* Are circumstances such that we should self-disable and not be a
+   proxy? We take a best-effort guess as to whether this device runs on
+   a battery or the data transfer might be expensive.
+
+   http://www.zytrax.com/tech/web/mobile_ids.html
+   http://googlewebmastercentral.blogspot.com/2011/03/mo-better-to-also-detect-mobile-user.html
+   http://search.cpan.org/~cmanley/Mobile-UserAgent-1.05/lib/Mobile/UserAgent.pm
+*/
+function flashproxy_should_disable() {
+    var ua;
+
+    ua = window.navigator.userAgent;
+    if (ua !== null) {
+        var UA_LIST = [
+            /\bmobile\b/i,
+            /\bandroid\b/i,
+            /\bopera mobi\b/i,
+        ];
+
+        for (var i = 0; i < UA_LIST.length; i++) {
+            var re = UA_LIST[i];
+
+            if (ua.match(re)) {
+                puts("Disable because User-Agent matches mobile pattern " + re + ".");
+                return true;
+            }
+        }
+    }
+
+    if (ua.match(/\bsafari\b/i) && !ua.match(/\bchrome\b/i)
+        && !ua.match(/\bversion\/[6789]\./i)) {
+        /* Disable before Safari 6.0 because it doesn't have the hybi/RFC type
+           of WebSockets. */
+        puts("Disable because User-Agent is Safari before 6.0.");
+        return true;
+    }
+
+    if (!WebSocket) {
+        /* No WebSocket support. */
+        puts("Disable because of no WebSocket support.");
+        return true;
+    }
+
+    return false;
+}
+
+function flashproxy_badge_insert() {
+    var fp;
+    var e;
+
+    fp = new FlashProxy();
+    if (flashproxy_should_disable())
+        fp.disable();
+
+    /* http://intertwingly.net/blog/2006/11/10/Thats-Not-Write for this trick to
+       insert right after the <script> element in the DOM. */
+    e = document;
+    while (e.lastChild && e.lastChild.nodeType === 1) {
+        e = e.lastChild;
+    }
+    e.parentNode.appendChild(fp.badge_elem);
+
+    return fp;
+}





More information about the tor-commits mailing list