commit c8bd5fd4fed1907817087ed8f7a6cba6511d1fb5 Author: David Fifield david@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://%5B1: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 = "✖"; - 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-... - http://search.cpan.org/~cmanley/Mobile-UserAgent-1.05/lib/Mobile/UserAgent.p... -*/ -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://%5B1: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 = "✖"; + 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-... + http://search.cpan.org/~cmanley/Mobile-UserAgent-1.05/lib/Mobile/UserAgent.p... +*/ +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; +}