[tor-commits] [flashproxy/js] Add rate limiting.

dcf at torproject.org dcf at torproject.org
Mon Apr 2 16:40:50 UTC 2012


commit 35d93b048ddad2760d4ba4ee8db8e49ab44524f6
Author: David Fifield <david at 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",





More information about the tor-commits mailing list