commit 35d93b048ddad2760d4ba4ee8db8e49ab44524f6 Author: David Fifield david@bamsoftware.com Date: Sat Mar 31 17:21:11 2012 -0700
Add rate limiting. --- flashproxy.js | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 files changed, 135 insertions(+), 7 deletions(-)
diff --git a/flashproxy.js b/flashproxy.js index 78b40aa..4803fce 100644 --- a/flashproxy.js +++ b/flashproxy.js @@ -27,6 +27,11 @@ * 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". */
var FLASHPROXY_INFO_URL = "https://crypto.stanford.edu/flashproxy/"; @@ -41,6 +46,11 @@ 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; + /* 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, @@ -143,6 +153,49 @@ 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) @@ -179,6 +232,7 @@ function make_websocket(addr) function FlashProxy() { var debug_div; + var rate_limit;
this.query = parse_query_string(window.location.search.substr(1));
@@ -204,16 +258,12 @@ function FlashProxy() } };
- var rate_limit = { - is_limited: function() { return false; }, - when: function() { return 0; } - }; - this.proxy_pairs = [];
this.start = function() { var client_addr; var relay_addr; + var rate_limit_bytes;
this.fac_addr = get_query_param_addr(this.query, "facilitator", DEFAULT_FACILITATOR_ADDR); if (!this.fac_addr) { @@ -236,6 +286,20 @@ function FlashProxy() return; }
+ if (this.query["ratelimit"] == "off") + rate_limit_bytes = undefined; + else + rate_limit_bytes = get_query_param_byte_count(this.query, "ratelimit", DEFAULT_RATE_LIMIT); + if (rate_limit_bytes === undefined) { + 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 { + rate_limit = new BucketRateLimit(rate_limit_bytes * RATE_LIMIT_HISTORY, RATE_LIMIT_HISTORY); + } + client_addr = get_query_param_addr(this.query, "client"); relay_addr = get_query_param_addr(this.query, "relay"); if (client_addr !== undefined && relay_addr !== undefined) { @@ -447,13 +511,19 @@ function FlashProxy()
busy = true; while (busy && !rate_limit.is_limited()) { + var chunk; + busy = false; if (is_open(this.client_s) && this.r2c_schedule.length > 0) { - this.client_s.send(this.r2c_schedule.shift()); + chunk = this.r2c_schedule.shift(); + rate_limit.update(chunk.length); + this.client_s.send(chunk); busy = true; } if (is_open(this.relay_s) && this.c2r_schedule.length > 0) { - this.relay_s.send(this.c2r_schedule.shift()); + chunk = this.c2r_schedule.shift(); + rate_limit.update(chunk.length); + this.relay_s.send(chunk); busy = true; } } @@ -473,6 +543,64 @@ function FlashProxy() } }
+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",