[tor-commits] [snowflake/master] Compile coffee files and remove them

arlo at torproject.org arlo at torproject.org
Wed Jul 10 15:58:46 UTC 2019


commit 31ad9566e64ca1236242966b7de6d045e25d837a
Author: Arlo Breault <arlolra at gmail.com>
Date:   Sat Jul 6 15:13:06 2019 +0200

    Compile coffee files and remove them
    
    With,
    
      ./node_modules/.bin/coffee -b -c Cakefile `find . -path ./node_modules -prune -o -name '*.coffee'`
---
 proxy/Cakefile                   |  83 ----------
 proxy/Cakefile.js                |  84 ++++++++++
 proxy/broker.coffee              |  90 -----------
 proxy/broker.js                  | 126 +++++++++++++++
 proxy/config.coffee              |  25 ---
 proxy/config.js                  |  43 ++++++
 proxy/init-badge.coffee          |  64 --------
 proxy/init-badge.js              |  75 +++++++++
 proxy/init-node.coffee           |  19 ---
 proxy/init-node.js               |  27 ++++
 proxy/init-webext.coffee         |  58 -------
 proxy/init-webext.js             |  76 +++++++++
 proxy/proxypair.coffee           | 185 ----------------------
 proxy/proxypair.js               | 262 ++++++++++++++++++++++++++++++++
 proxy/shims.coffee               |  35 -----
 proxy/shims.js                   |  34 +++++
 proxy/snowflake.coffee           | 132 ----------------
 proxy/snowflake.js               | 182 ++++++++++++++++++++++
 proxy/spec/broker.spec.coffee    |  92 -----------
 proxy/spec/broker.spec.js        | 119 +++++++++++++++
 proxy/spec/init.spec.coffee      |  28 ----
 proxy/spec/init.spec.js          |  34 +++++
 proxy/spec/proxypair.spec.coffee | 125 ---------------
 proxy/spec/proxypair.spec.js     | 143 +++++++++++++++++
 proxy/spec/snowflake.spec.coffee |  67 --------
 proxy/spec/snowflake.spec.js     | 114 ++++++++++++++
 proxy/spec/ui.spec.coffee        |  57 -------
 proxy/spec/ui.spec.js            |  84 ++++++++++
 proxy/spec/util.spec.coffee      | 236 ----------------------------
 proxy/spec/util.spec.js          | 254 +++++++++++++++++++++++++++++++
 proxy/spec/websocket.spec.coffee |  39 -----
 proxy/spec/websocket.spec.js     |  32 ++++
 proxy/ui.coffee                  | 125 ---------------
 proxy/ui.js                      | 197 ++++++++++++++++++++++++
 proxy/util.coffee                | 204 -------------------------
 proxy/util.js                    | 321 +++++++++++++++++++++++++++++++++++++++
 proxy/websocket.coffee           |  61 --------
 proxy/websocket.js               |  70 +++++++++
 38 files changed, 2277 insertions(+), 1725 deletions(-)

diff --git a/proxy/Cakefile b/proxy/Cakefile
deleted file mode 100644
index ba2e5ec..0000000
--- a/proxy/Cakefile
+++ /dev/null
@@ -1,83 +0,0 @@
-fs = require 'fs'
-{ exec, spawn, execSync } = require 'child_process'
-
-# All coffeescript files required.
-FILES = [
-  'broker.coffee'
-  'config.coffee'
-  'proxypair.coffee'
-  'snowflake.coffee'
-  'ui.coffee'
-  'util.coffee'
-  'websocket.coffee'
-
-  'shims.coffee'
-]
-
-INITS = [
-  'init-badge.coffee'
-  'init-node.coffee'
-  'init-webext.coffee'
-]
-
-FILES_SPEC = [
-  'spec/broker.spec.coffee'
-  'spec/init.spec.coffee'
-  'spec/proxypair.spec.coffee'
-  'spec/snowflake.spec.coffee'
-  'spec/ui.spec.coffee'
-  'spec/util.spec.coffee'
-  'spec/websocket.spec.coffee'
-]
-
-OUTFILE = 'snowflake.js'
-STATIC = 'static'
-
-copyStaticFiles = ->
-  exec 'cp ' + STATIC + '/* build/'
-
-compileCoffee = (outDir, init) ->
-  files = FILES.concat('init-' + init + '.coffee')
-  exec 'cat ' + files.join(' ') + ' | coffee -cs > ' + outDir + '/' + OUTFILE, (err, stdout, stderr) ->
-    throw err if err
-
-task 'test', 'snowflake unit tests', ->
-  exec 'mkdir -p test'
-  exec 'jasmine init >&-'
-  # Simply concat all the files because we're not using node exports.
-  jasmineFiles = FILES.concat('init-badge.coffee', FILES_SPEC)
-  outFile = 'test/bundle.spec.coffee'
-  exec 'echo "TESTING = true" > ' + outFile
-  exec 'cat ' + jasmineFiles.join(' ') +  ' | cat >> ' + outFile
-  execSync 'coffee -cb ' + outFile
-  proc = spawn 'jasmine', ['test/bundle.spec.js'], {
-    stdio: 'inherit'
-  }
-  proc.on "exit", (code) -> process.exit code
-
-task 'build', 'build the snowflake proxy', ->
-  exec 'mkdir -p build'
-  copyStaticFiles()
-  compileCoffee('build', 'badge')
-  console.log 'Snowflake prepared.'
-
-task 'webext', 'build the webextension', ->
-  exec 'mkdir -p webext'
-  compileCoffee('webext', 'webext')
-  console.log 'Webextension prepared.'
-
-task 'node', 'build the node binary', ->
-  exec 'mkdir -p build'
-  compileCoffee('build', 'node')
-  console.log 'Node prepared.'
-
-task 'lint', 'ensure idiomatic coffeescript', ->
-  filesAll = FILES.concat(INITS, FILES_SPEC)
-  proc = spawn 'coffeelint', filesAll, {
-    file: 'coffeelint.json'
-    stdio: 'inherit'
-  }
-  proc.on "exit", (code) -> process.exit code
-
-task 'clean', 'remove all built files', ->
-  exec 'rm -r build'
diff --git a/proxy/Cakefile.js b/proxy/Cakefile.js
new file mode 100644
index 0000000..c7bc625
--- /dev/null
+++ b/proxy/Cakefile.js
@@ -0,0 +1,84 @@
+// Generated by CoffeeScript 2.4.1
+var FILES, FILES_SPEC, INITS, OUTFILE, STATIC, compileCoffee, copyStaticFiles, exec, execSync, fs, spawn;
+
+fs = require('fs');
+
+({exec, spawn, execSync} = require('child_process'));
+
+// All coffeescript files required.
+FILES = ['broker.coffee', 'config.coffee', 'proxypair.coffee', 'snowflake.coffee', 'ui.coffee', 'util.coffee', 'websocket.coffee', 'shims.coffee'];
+
+INITS = ['init-badge.coffee', 'init-node.coffee', 'init-webext.coffee'];
+
+FILES_SPEC = ['spec/broker.spec.coffee', 'spec/init.spec.coffee', 'spec/proxypair.spec.coffee', 'spec/snowflake.spec.coffee', 'spec/ui.spec.coffee', 'spec/util.spec.coffee', 'spec/websocket.spec.coffee'];
+
+OUTFILE = 'snowflake.js';
+
+STATIC = 'static';
+
+copyStaticFiles = function() {
+  return exec('cp ' + STATIC + '/* build/');
+};
+
+compileCoffee = function(outDir, init) {
+  var files;
+  files = FILES.concat('init-' + init + '.coffee');
+  return exec('cat ' + files.join(' ') + ' | coffee -cs > ' + outDir + '/' + OUTFILE, function(err, stdout, stderr) {
+    if (err) {
+      throw err;
+    }
+  });
+};
+
+task('test', 'snowflake unit tests', function() {
+  var jasmineFiles, outFile, proc;
+  exec('mkdir -p test');
+  exec('jasmine init >&-');
+  // Simply concat all the files because we're not using node exports.
+  jasmineFiles = FILES.concat('init-badge.coffee', FILES_SPEC);
+  outFile = 'test/bundle.spec.coffee';
+  exec('echo "TESTING = true" > ' + outFile);
+  exec('cat ' + jasmineFiles.join(' ') + ' | cat >> ' + outFile);
+  execSync('coffee -cb ' + outFile);
+  proc = spawn('jasmine', ['test/bundle.spec.js'], {
+    stdio: 'inherit'
+  });
+  return proc.on("exit", function(code) {
+    return process.exit(code);
+  });
+});
+
+task('build', 'build the snowflake proxy', function() {
+  exec('mkdir -p build');
+  copyStaticFiles();
+  compileCoffee('build', 'badge');
+  return console.log('Snowflake prepared.');
+});
+
+task('webext', 'build the webextension', function() {
+  exec('mkdir -p webext');
+  compileCoffee('webext', 'webext');
+  return console.log('Webextension prepared.');
+});
+
+task('node', 'build the node binary', function() {
+  exec('mkdir -p build');
+  compileCoffee('build', 'node');
+  return console.log('Node prepared.');
+});
+
+task('lint', 'ensure idiomatic coffeescript', function() {
+  var filesAll, proc;
+  filesAll = FILES.concat(INITS, FILES_SPEC);
+  proc = spawn('coffeelint', filesAll, {
+    file: 'coffeelint.json',
+    stdio: 'inherit'
+  });
+  return proc.on("exit", function(code) {
+    return process.exit(code);
+  });
+});
+
+task('clean', 'remove all built files', function() {
+  return exec('rm -r build');
+});
diff --git a/proxy/broker.coffee b/proxy/broker.coffee
deleted file mode 100644
index ca8a2e5..0000000
--- a/proxy/broker.coffee
+++ /dev/null
@@ -1,90 +0,0 @@
-###
-Communication with the snowflake broker.
-
-Browser snowflakes must register with the broker in order
-to get assigned to clients.
-###
-
-# Represents a broker running remotely.
-class Broker
-  @STATUS:
-    OK: 200
-    GONE: 410
-    GATEWAY_TIMEOUT: 504
-
-  @MESSAGE:
-    TIMEOUT: 'Timed out waiting for a client offer.'
-    UNEXPECTED: 'Unexpected status.'
-
-  clients: 0
-
-  # When interacting with the Broker, snowflake must generate a unique session
-  # ID so the Broker can keep track of each proxy's signalling channels.
-  # On construction, this Broker object does not do anything until
-  # |getClientOffer| is called.
-  constructor: (@url) ->
-    @clients = 0
-    # Ensure url has the right protocol + trailing slash.
-    @url = 'http://' + @url if 0 == @url.indexOf('localhost', 0)
-    @url = 'https://' + @url if 0 != @url.indexOf('http', 0)
-    @url += '/' if '/' != @url.substr -1
-
-  # Promises some client SDP Offer.
-  # Registers this Snowflake with the broker using an HTTP POST request, and
-  # waits for a response containing some client offer that the Broker chooses
-  # for this proxy..
-  # TODO: Actually support multiple clients.
-  getClientOffer: (id) =>
-    new Promise (fulfill, reject) =>
-      xhr = new XMLHttpRequest()
-      xhr.onreadystatechange = ->
-        return if xhr.DONE != xhr.readyState
-        switch xhr.status
-          when Broker.STATUS.OK
-            fulfill xhr.responseText  # Should contain offer.
-          when Broker.STATUS.GATEWAY_TIMEOUT
-            reject Broker.MESSAGE.TIMEOUT
-          else
-            log 'Broker ERROR: Unexpected ' + xhr.status +
-                ' - ' + xhr.statusText
-            snowflake.ui.setStatus ' failure. Please refresh.'
-            reject Broker.MESSAGE.UNEXPECTED
-      @_xhr = xhr  # Used by spec to fake async Broker interaction
-      @_postRequest id, xhr, 'proxy', id
-
-  # Assumes getClientOffer happened, and a WebRTC SDP answer has been generated.
-  # Sends it back to the broker, which passes it to back to the original client.
-  sendAnswer: (id, answer) ->
-    dbg id + ' - Sending answer back to broker...\n'
-    dbg answer.sdp
-    xhr = new XMLHttpRequest()
-    xhr.onreadystatechange = ->
-      return if xhr.DONE != xhr.readyState
-      switch xhr.status
-        when Broker.STATUS.OK
-          dbg 'Broker: Successfully replied with answer.'
-          dbg xhr.responseText
-        when Broker.STATUS.GONE
-          dbg 'Broker: No longer valid to reply with answer.'
-        else
-          dbg 'Broker ERROR: Unexpected ' + xhr.status +
-              ' - ' + xhr.statusText
-          snowflake.ui.setStatus ' failure. Please refresh.'
-    @_postRequest id, xhr, 'answer', JSON.stringify(answer)
-
-  # urlSuffix for the broker is different depending on what action
-  # is desired.
-  _postRequest: (id, xhr, urlSuffix, payload) =>
-    try
-      xhr.open 'POST', @url + urlSuffix
-      xhr.setRequestHeader('X-Session-ID', id)
-    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 xhr. The exception message is like "Component
-      returned failure code: 0x805e0006 [nsIXMLHttpRequest.open]" on Firefox.
-      ###
-      log 'Broker: exception while connecting: ' + err.message
-      return
-    xhr.send payload
diff --git a/proxy/broker.js b/proxy/broker.js
new file mode 100644
index 0000000..f7af5bb
--- /dev/null
+++ b/proxy/broker.js
@@ -0,0 +1,126 @@
+// Generated by CoffeeScript 2.4.1
+/*
+Communication with the snowflake broker.
+
+Browser snowflakes must register with the broker in order
+to get assigned to clients.
+*/
+var Broker;
+
+Broker = (function() {
+  // Represents a broker running remotely.
+  class Broker {
+    // When interacting with the Broker, snowflake must generate a unique session
+    // ID so the Broker can keep track of each proxy's signalling channels.
+    // On construction, this Broker object does not do anything until
+    // |getClientOffer| is called.
+    constructor(url) {
+      // Promises some client SDP Offer.
+      // Registers this Snowflake with the broker using an HTTP POST request, and
+      // waits for a response containing some client offer that the Broker chooses
+      // for this proxy..
+      // TODO: Actually support multiple clients.
+      this.getClientOffer = this.getClientOffer.bind(this);
+      // urlSuffix for the broker is different depending on what action
+      // is desired.
+      this._postRequest = this._postRequest.bind(this);
+      this.url = url;
+      this.clients = 0;
+      if (0 === this.url.indexOf('localhost', 0)) {
+        // Ensure url has the right protocol + trailing slash.
+        this.url = 'http://' + this.url;
+      }
+      if (0 !== this.url.indexOf('http', 0)) {
+        this.url = 'https://' + this.url;
+      }
+      if ('/' !== this.url.substr(-1)) {
+        this.url += '/';
+      }
+    }
+
+    getClientOffer(id) {
+      return new Promise((fulfill, reject) => {
+        var xhr;
+        xhr = new XMLHttpRequest();
+        xhr.onreadystatechange = function() {
+          if (xhr.DONE !== xhr.readyState) {
+            return;
+          }
+          switch (xhr.status) {
+            case Broker.STATUS.OK:
+              return fulfill(xhr.responseText); // Should contain offer.
+            case Broker.STATUS.GATEWAY_TIMEOUT:
+              return reject(Broker.MESSAGE.TIMEOUT);
+            default:
+              log('Broker ERROR: Unexpected ' + xhr.status + ' - ' + xhr.statusText);
+              snowflake.ui.setStatus(' failure. Please refresh.');
+              return reject(Broker.MESSAGE.UNEXPECTED);
+          }
+        };
+        this._xhr = xhr; // Used by spec to fake async Broker interaction
+        return this._postRequest(id, xhr, 'proxy', id);
+      });
+    }
+
+    // Assumes getClientOffer happened, and a WebRTC SDP answer has been generated.
+    // Sends it back to the broker, which passes it to back to the original client.
+    sendAnswer(id, answer) {
+      var xhr;
+      dbg(id + ' - Sending answer back to broker...\n');
+      dbg(answer.sdp);
+      xhr = new XMLHttpRequest();
+      xhr.onreadystatechange = function() {
+        if (xhr.DONE !== xhr.readyState) {
+          return;
+        }
+        switch (xhr.status) {
+          case Broker.STATUS.OK:
+            dbg('Broker: Successfully replied with answer.');
+            return dbg(xhr.responseText);
+          case Broker.STATUS.GONE:
+            return dbg('Broker: No longer valid to reply with answer.');
+          default:
+            dbg('Broker ERROR: Unexpected ' + xhr.status + ' - ' + xhr.statusText);
+            return snowflake.ui.setStatus(' failure. Please refresh.');
+        }
+      };
+      return this._postRequest(id, xhr, 'answer', JSON.stringify(answer));
+    }
+
+    _postRequest(id, xhr, urlSuffix, payload) {
+      var err;
+      try {
+        xhr.open('POST', this.url + urlSuffix);
+        xhr.setRequestHeader('X-Session-ID', id);
+      } catch (error) {
+        err = error;
+        /*
+        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 xhr. The exception message is like "Component
+        returned failure code: 0x805e0006 [nsIXMLHttpRequest.open]" on Firefox.
+        */
+        log('Broker: exception while connecting: ' + err.message);
+        return;
+      }
+      return xhr.send(payload);
+    }
+
+  };
+
+  Broker.STATUS = {
+    OK: 200,
+    GONE: 410,
+    GATEWAY_TIMEOUT: 504
+  };
+
+  Broker.MESSAGE = {
+    TIMEOUT: 'Timed out waiting for a client offer.',
+    UNEXPECTED: 'Unexpected status.'
+  };
+
+  Broker.prototype.clients = 0;
+
+  return Broker;
+
+}).call(this);
diff --git a/proxy/config.coffee b/proxy/config.coffee
deleted file mode 100644
index 43dca56..0000000
--- a/proxy/config.coffee
+++ /dev/null
@@ -1,25 +0,0 @@
-class Config
-  brokerUrl: 'snowflake-broker.bamsoftware.com'
-  relayAddr:
-    host: 'snowflake.bamsoftware.com'
-    port: '443'
-    # Original non-wss relay:
-    # host: '192.81.135.242'
-    # port: 9902
-
-  cookieName: "snowflake-allow"
-
-  # Bytes per second. Set to undefined to disable limit.
-  rateLimitBytes: undefined
-  minRateLimit: 10 * 1024
-  rateLimitHistory: 5.0
-  defaultBrokerPollInterval: 5.0 * 1000
-
-  maxNumClients: 1
-  connectionsPerClient: 1
-
-  # TODO: Different ICE servers.
-  pcConfig:
-    iceServers: [
-      { urls: ['stun:stun.l.google.com:19302'] }
-    ]
diff --git a/proxy/config.js b/proxy/config.js
new file mode 100644
index 0000000..69d7b75
--- /dev/null
+++ b/proxy/config.js
@@ -0,0 +1,43 @@
+// Generated by CoffeeScript 2.4.1
+var Config;
+
+Config = (function() {
+  class Config {};
+
+  Config.prototype.brokerUrl = 'snowflake-broker.bamsoftware.com';
+
+  Config.prototype.relayAddr = {
+    host: 'snowflake.bamsoftware.com',
+    port: '443'
+  };
+
+  // Original non-wss relay:
+  // host: '192.81.135.242'
+  // port: 9902
+  Config.prototype.cookieName = "snowflake-allow";
+
+  // Bytes per second. Set to undefined to disable limit.
+  Config.prototype.rateLimitBytes = void 0;
+
+  Config.prototype.minRateLimit = 10 * 1024;
+
+  Config.prototype.rateLimitHistory = 5.0;
+
+  Config.prototype.defaultBrokerPollInterval = 5.0 * 1000;
+
+  Config.prototype.maxNumClients = 1;
+
+  Config.prototype.connectionsPerClient = 1;
+
+  // TODO: Different ICE servers.
+  Config.prototype.pcConfig = {
+    iceServers: [
+      {
+        urls: ['stun:stun.l.google.com:19302']
+      }
+    ]
+  };
+
+  return Config;
+
+}).call(this);
diff --git a/proxy/init-badge.coffee b/proxy/init-badge.coffee
deleted file mode 100644
index c4c9604..0000000
--- a/proxy/init-badge.coffee
+++ /dev/null
@@ -1,64 +0,0 @@
-###
-Entry point.
-###
-
-if (not TESTING? or not TESTING) and not Util.featureDetect()
-  console.log 'webrtc feature not detected. shutting down'
-  return
-
-snowflake = null
-
-query = Query.parse(location)
-debug = Params.getBool(query, 'debug', false)
-silenceNotifications = Params.getBool(query, 'silent', false)
-
-# Log to both console and UI if applicable.
-# Requires that the snowflake and UI objects are hooked up in order to
-# log to console.
-log = (msg) ->
-  console.log 'Snowflake: ' + msg
-  snowflake?.ui.log msg
-
-dbg = (msg) -> log msg if debug or (snowflake?.ui instanceof DebugUI)
-
-init = () ->
-  config = new Config
-
-  if 'off' != query['ratelimit']
-    config.rateLimitBytes = Params.getByteCount(
-      query,'ratelimit', config.rateLimitBytes
-    )
-
-  ui = null
-  if (document.getElementById('badge') != null)
-    ui = new BadgeUI()
-  else if (document.getElementById('status') != null)
-    ui = new DebugUI()
-  else
-    ui = new UI()
-
-  broker = new Broker config.brokerUrl
-  snowflake = new Snowflake config, ui, broker
-
-  log '== snowflake proxy =='
-  if Util.snowflakeIsDisabled(config.cookieName)
-    # Do not activate the proxy if any number of conditions are true.
-    log 'Currently not active.'
-    return
-
-  # Otherwise, begin setting up WebRTC and acting as a proxy.
-  dbg 'Contacting Broker at ' + broker.url
-  snowflake.setRelayAddr config.relayAddr
-  snowflake.beginWebRTC()
-
-# Notification of closing tab with active proxy.
-window.onbeforeunload = ->
-  if !silenceNotifications && Snowflake.MODE.WEBRTC_READY == snowflake.state
-    return Snowflake.MESSAGE.CONFIRMATION
-  null
-
-window.onunload = ->
-  snowflake.disable()
-  null
-
-window.onload = init
diff --git a/proxy/init-badge.js b/proxy/init-badge.js
new file mode 100644
index 0000000..136835b
--- /dev/null
+++ b/proxy/init-badge.js
@@ -0,0 +1,75 @@
+// Generated by CoffeeScript 2.4.1
+/*
+Entry point.
+*/
+var dbg, debug, init, log, query, silenceNotifications, snowflake;
+
+if (((typeof TESTING === "undefined" || TESTING === null) || !TESTING) && !Util.featureDetect()) {
+  console.log('webrtc feature not detected. shutting down');
+  return;
+}
+
+snowflake = null;
+
+query = Query.parse(location);
+
+debug = Params.getBool(query, 'debug', false);
+
+silenceNotifications = Params.getBool(query, 'silent', false);
+
+// Log to both console and UI if applicable.
+// Requires that the snowflake and UI objects are hooked up in order to
+// log to console.
+log = function(msg) {
+  console.log('Snowflake: ' + msg);
+  return snowflake != null ? snowflake.ui.log(msg) : void 0;
+};
+
+dbg = function(msg) {
+  if (debug || ((snowflake != null ? snowflake.ui : void 0) instanceof DebugUI)) {
+    return log(msg);
+  }
+};
+
+init = function() {
+  var broker, config, ui;
+  config = new Config;
+  if ('off' !== query['ratelimit']) {
+    config.rateLimitBytes = Params.getByteCount(query, 'ratelimit', config.rateLimitBytes);
+  }
+  ui = null;
+  if (document.getElementById('badge') !== null) {
+    ui = new BadgeUI();
+  } else if (document.getElementById('status') !== null) {
+    ui = new DebugUI();
+  } else {
+    ui = new UI();
+  }
+  broker = new Broker(config.brokerUrl);
+  snowflake = new Snowflake(config, ui, broker);
+  log('== snowflake proxy ==');
+  if (Util.snowflakeIsDisabled(config.cookieName)) {
+    // Do not activate the proxy if any number of conditions are true.
+    log('Currently not active.');
+    return;
+  }
+  // Otherwise, begin setting up WebRTC and acting as a proxy.
+  dbg('Contacting Broker at ' + broker.url);
+  snowflake.setRelayAddr(config.relayAddr);
+  return snowflake.beginWebRTC();
+};
+
+// Notification of closing tab with active proxy.
+window.onbeforeunload = function() {
+  if (!silenceNotifications && Snowflake.MODE.WEBRTC_READY === snowflake.state) {
+    return Snowflake.MESSAGE.CONFIRMATION;
+  }
+  return null;
+};
+
+window.onunload = function() {
+  snowflake.disable();
+  return null;
+};
+
+window.onload = init;
diff --git a/proxy/init-node.coffee b/proxy/init-node.coffee
deleted file mode 100644
index 814e3fc..0000000
--- a/proxy/init-node.coffee
+++ /dev/null
@@ -1,19 +0,0 @@
-###
-Entry point.
-###
-
-config = new Config
-ui = new UI()
-broker = new Broker config.brokerUrl
-snowflake = new Snowflake config, ui, broker
-
-log = (msg) ->
-  console.log 'Snowflake: ' + msg
-
-dbg = log
-
-log '== snowflake proxy =='
-dbg 'Contacting Broker at ' + broker.url
-
-snowflake.setRelayAddr config.relayAddr
-snowflake.beginWebRTC()
diff --git a/proxy/init-node.js b/proxy/init-node.js
new file mode 100644
index 0000000..c6e2e52
--- /dev/null
+++ b/proxy/init-node.js
@@ -0,0 +1,27 @@
+// Generated by CoffeeScript 2.4.1
+/*
+Entry point.
+*/
+var broker, config, dbg, log, snowflake, ui;
+
+config = new Config;
+
+ui = new UI();
+
+broker = new Broker(config.brokerUrl);
+
+snowflake = new Snowflake(config, ui, broker);
+
+log = function(msg) {
+  return console.log('Snowflake: ' + msg);
+};
+
+dbg = log;
+
+log('== snowflake proxy ==');
+
+dbg('Contacting Broker at ' + broker.url);
+
+snowflake.setRelayAddr(config.relayAddr);
+
+snowflake.beginWebRTC();
diff --git a/proxy/init-webext.coffee b/proxy/init-webext.coffee
deleted file mode 100644
index 716604f..0000000
--- a/proxy/init-webext.coffee
+++ /dev/null
@@ -1,58 +0,0 @@
-###
-Entry point.
-###
-
-debug = false
-snowflake = null
-config = null
-broker = null
-ui = null
-
-# Log to both console and UI if applicable.
-# Requires that the snowflake and UI objects are hooked up in order to
-# log to console.
-log = (msg) ->
-  console.log 'Snowflake: ' + msg
-  snowflake?.ui.log msg
-
-dbg = (msg) -> log msg if debug
-
-if not Util.featureDetect()
-  chrome.runtime.onConnect.addListener (port) ->
-    port.postMessage
-      missingFeature: true
-  return
-
-init = () ->
-  config = new Config
-  ui = new WebExtUI()
-  broker = new Broker config.brokerUrl
-  snowflake = new Snowflake config, ui, broker
-
-  log '== snowflake proxy =='
-  ui.initToggle()
-
-update = () ->
-  if !ui.enabled
-    # Do not activate the proxy if any number of conditions are true.
-    snowflake.disable()
-    log 'Currently not active.'
-    return
-
-  # Otherwise, begin setting up WebRTC and acting as a proxy.
-  dbg 'Contacting Broker at ' + broker.url
-  log 'Starting snowflake'
-  snowflake.setRelayAddr config.relayAddr
-  snowflake.beginWebRTC()
-
-# Notification of closing tab with active proxy.
-window.onbeforeunload = ->
-  if !silenceNotifications && Snowflake.MODE.WEBRTC_READY == snowflake.state
-    return Snowflake.MESSAGE.CONFIRMATION
-  null
-
-window.onunload = ->
-  snowflake.disable()
-  null
-
-window.onload = init
diff --git a/proxy/init-webext.js b/proxy/init-webext.js
new file mode 100644
index 0000000..4b23403
--- /dev/null
+++ b/proxy/init-webext.js
@@ -0,0 +1,76 @@
+// Generated by CoffeeScript 2.4.1
+/*
+Entry point.
+*/
+var broker, config, dbg, debug, init, log, snowflake, ui, update;
+
+debug = false;
+
+snowflake = null;
+
+config = null;
+
+broker = null;
+
+ui = null;
+
+// Log to both console and UI if applicable.
+// Requires that the snowflake and UI objects are hooked up in order to
+// log to console.
+log = function(msg) {
+  console.log('Snowflake: ' + msg);
+  return snowflake != null ? snowflake.ui.log(msg) : void 0;
+};
+
+dbg = function(msg) {
+  if (debug) {
+    return log(msg);
+  }
+};
+
+if (!Util.featureDetect()) {
+  chrome.runtime.onConnect.addListener(function(port) {
+    return port.postMessage({
+      missingFeature: true
+    });
+  });
+  return;
+}
+
+init = function() {
+  config = new Config;
+  ui = new WebExtUI();
+  broker = new Broker(config.brokerUrl);
+  snowflake = new Snowflake(config, ui, broker);
+  log('== snowflake proxy ==');
+  return ui.initToggle();
+};
+
+update = function() {
+  if (!ui.enabled) {
+    // Do not activate the proxy if any number of conditions are true.
+    snowflake.disable();
+    log('Currently not active.');
+    return;
+  }
+  // Otherwise, begin setting up WebRTC and acting as a proxy.
+  dbg('Contacting Broker at ' + broker.url);
+  log('Starting snowflake');
+  snowflake.setRelayAddr(config.relayAddr);
+  return snowflake.beginWebRTC();
+};
+
+// Notification of closing tab with active proxy.
+window.onbeforeunload = function() {
+  if (!silenceNotifications && Snowflake.MODE.WEBRTC_READY === snowflake.state) {
+    return Snowflake.MESSAGE.CONFIRMATION;
+  }
+  return null;
+};
+
+window.onunload = function() {
+  snowflake.disable();
+  return null;
+};
+
+window.onload = init;
diff --git a/proxy/proxypair.coffee b/proxy/proxypair.coffee
deleted file mode 100644
index b66ba4f..0000000
--- a/proxy/proxypair.coffee
+++ /dev/null
@@ -1,185 +0,0 @@
-###
-Represents a single:
-
-   client <-- webrtc --> snowflake <-- websocket --> relay
-
-Every ProxyPair has a Snowflake ID, which is necessary when responding to the
-Broker with an WebRTC answer.
-###
-
-class ProxyPair
-  MAX_BUFFER: 10 * 1024 * 1024
-  pc:          null
-  client:      null  # WebRTC Data channel
-  relay:       null   # websocket
-  timer:       0
-  running:     true
-  active:      false  # Whether serving a client.
-  flush_timeout_id: null
-  onCleanup:   null
-  id:          null
-
-  ###
-  Constructs a ProxyPair where:
-  - @relayAddr is the destination relay
-  - @rateLimit specifies a rate limit on traffic
-  ###
-  constructor: (@relayAddr, @rateLimit, @pcConfig) ->
-    @id = Util.genSnowflakeID()
-    @c2rSchedule = []
-    @r2cSchedule = []
-
-  # Prepare a WebRTC PeerConnection and await for an SDP offer.
-  begin: ->
-    @pc = new PeerConnection @pcConfig, {
-      optional: [
-        { DtlsSrtpKeyAgreement: true }
-        { RtpDataChannels: false }
-      ] }
-    @pc.onicecandidate = (evt) =>
-      # Browser sends a null candidate once the ICE gathering completes.
-      if null == evt.candidate
-        # TODO: Use a promise.all to tell Snowflake about all offers at once,
-        # once multiple proxypairs are supported.
-        dbg 'Finished gathering ICE candidates.'
-        snowflake.broker.sendAnswer @id, @pc.localDescription
-    # OnDataChannel triggered remotely from the client when connection succeeds.
-    @pc.ondatachannel = (dc) =>
-      channel = dc.channel
-      dbg 'Data Channel established...'
-      @prepareDataChannel channel
-      @client = channel
-
-  receiveWebRTCOffer: (offer) ->
-    if 'offer' != offer.type
-      log 'Invalid SDP received -- was not an offer.'
-      return false
-    try
-      err = @pc.setRemoteDescription offer
-    catch e
-      log 'Invalid SDP message.'
-      return false
-    dbg 'SDP ' + offer.type + ' successfully received.'
-    true
-
-  # Given a WebRTC DataChannel, prepare callbacks.
-  prepareDataChannel: (channel) =>
-    channel.onopen = =>
-      log 'WebRTC DataChannel opened!'
-      snowflake.state = Snowflake.MODE.WEBRTC_READY
-      snowflake.ui.setActive true
-      # This is the point when the WebRTC datachannel is done, so the next step
-      # is to establish websocket to the server.
-      @connectRelay()
-    channel.onclose = =>
-      log 'WebRTC DataChannel closed.'
-      snowflake.ui.setStatus 'disconnected by webrtc.'
-      snowflake.ui.setActive false
-      snowflake.state = Snowflake.MODE.INIT
-      @flush()
-      @close()
-    channel.onerror = -> log 'Data channel error!'
-    channel.binaryType = "arraybuffer"
-    channel.onmessage = @onClientToRelayMessage
-
-  # Assumes WebRTC datachannel is connected.
-  connectRelay: =>
-    dbg 'Connecting to relay...'
-
-    # Get a remote IP address from the PeerConnection, if possible. Add it to
-    # the WebSocket URL's query string if available.
-    # MDN marks remoteDescription as "experimental". However the other two
-    # options, currentRemoteDescription and pendingRemoteDescription, which
-    # are not marked experimental, were undefined when I tried them in Firefox
-    # 52.2.0.
-    # https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/remoteDescription
-    peer_ip = Parse.ipFromSDP(@pc.remoteDescription?.sdp)
-    params = []
-    if peer_ip?
-      params.push(["client_ip", peer_ip])
-
-    @relay = WS.makeWebsocket @relayAddr, params
-    @relay.label = 'websocket-relay'
-    @relay.onopen = =>
-      if @timer
-        clearTimeout @timer
-        @timer = 0
-      log @relay.label + ' connected!'
-      snowflake.ui.setStatus 'connected'
-    @relay.onclose = =>
-      log @relay.label + ' closed.'
-      snowflake.ui.setStatus 'disconnected.'
-      snowflake.ui.setActive false
-      snowflake.state = Snowflake.MODE.INIT
-      @flush()
-      @close()
-    @relay.onerror = @onError
-    @relay.onmessage = @onRelayToClientMessage
-    # TODO: Better websocket timeout handling.
-    @timer = setTimeout((=>
-      return if 0 == @timer
-      log @relay.label + ' timed out connecting.'
-      @relay.onclose()), 5000)
-
-  # WebRTC --> websocket
-  onClientToRelayMessage: (msg) =>
-    dbg 'WebRTC --> websocket data: ' + msg.data.byteLength + ' bytes'
-    @c2rSchedule.push msg.data
-    @flush()
-
-  # websocket --> WebRTC
-  onRelayToClientMessage: (event) =>
-    dbg 'websocket --> WebRTC data: ' + event.data.byteLength + ' bytes'
-    @r2cSchedule.push event.data
-    @flush()
-
-  onError: (event) =>
-    ws = event.target
-    log ws.label + ' error.'
-    @close()
-
-  # Close both WebRTC and websocket.
-  close: ->
-    if @timer
-      clearTimeout @timer
-      @timer = 0
-    @running = false
-    @client.close() if @webrtcIsReady()
-    @relay.close() if @relayIsReady()
-    relay = null
-    @onCleanup()
-
-  # Send as much data in both directions as the rate limit currently allows.
-  flush: =>
-    clearTimeout @flush_timeout_id if @flush_timeout_id
-    @flush_timeout_id = null
-    busy = true
-    checkChunks = =>
-      busy = false
-      # WebRTC --> websocket
-      if @relayIsReady() &&
-         @relay.bufferedAmount < @MAX_BUFFER &&
-         @c2rSchedule.length > 0
-        chunk = @c2rSchedule.shift()
-        @rateLimit.update chunk.byteLength
-        @relay.send chunk
-        busy = true
-      # websocket --> WebRTC
-      if @webrtcIsReady() &&
-         @client.bufferedAmount < @MAX_BUFFER &&
-         @r2cSchedule.length > 0
-        chunk = @r2cSchedule.shift()
-        @rateLimit.update chunk.byteLength
-        @client.send chunk
-        busy = true
-
-    checkChunks() while busy  && !@rateLimit.isLimited()
-
-    if @r2cSchedule.length > 0 || @c2rSchedule.length > 0 ||
-       (@relayIsReady()  && @relay.bufferedAmount > 0) ||
-       (@webrtcIsReady() && @client.bufferedAmount > 0)
-      @flush_timeout_id = setTimeout @flush,  @rateLimit.when() * 1000
-
-  webrtcIsReady: -> null != @client && 'open' == @client.readyState
-  relayIsReady: -> (null != @relay) && (WebSocket.OPEN == @relay.readyState)
-  isClosed: (ws) -> undefined == ws || WebSocket.CLOSED == ws.readyState
diff --git a/proxy/proxypair.js b/proxy/proxypair.js
new file mode 100644
index 0000000..40f7b38
--- /dev/null
+++ b/proxy/proxypair.js
@@ -0,0 +1,262 @@
+// Generated by CoffeeScript 2.4.1
+/*
+Represents a single:
+
+   client <-- webrtc --> snowflake <-- websocket --> relay
+
+Every ProxyPair has a Snowflake ID, which is necessary when responding to the
+Broker with an WebRTC answer.
+*/
+var ProxyPair;
+
+ProxyPair = (function() {
+  class ProxyPair {
+    /*
+    Constructs a ProxyPair where:
+    - @relayAddr is the destination relay
+    - @rateLimit specifies a rate limit on traffic
+    */
+    constructor(relayAddr, rateLimit, pcConfig) {
+      // Given a WebRTC DataChannel, prepare callbacks.
+      this.prepareDataChannel = this.prepareDataChannel.bind(this);
+      // Assumes WebRTC datachannel is connected.
+      this.connectRelay = this.connectRelay.bind(this);
+      // WebRTC --> websocket
+      this.onClientToRelayMessage = this.onClientToRelayMessage.bind(this);
+      // websocket --> WebRTC
+      this.onRelayToClientMessage = this.onRelayToClientMessage.bind(this);
+      this.onError = this.onError.bind(this);
+      // Send as much data in both directions as the rate limit currently allows.
+      this.flush = this.flush.bind(this);
+      this.relayAddr = relayAddr;
+      this.rateLimit = rateLimit;
+      this.pcConfig = pcConfig;
+      this.id = Util.genSnowflakeID();
+      this.c2rSchedule = [];
+      this.r2cSchedule = [];
+    }
+
+    // Prepare a WebRTC PeerConnection and await for an SDP offer.
+    begin() {
+      this.pc = new PeerConnection(this.pcConfig, {
+        optional: [
+          {
+            DtlsSrtpKeyAgreement: true
+          },
+          {
+            RtpDataChannels: false
+          }
+        ]
+      });
+      this.pc.onicecandidate = (evt) => {
+        // Browser sends a null candidate once the ICE gathering completes.
+        if (null === evt.candidate) {
+          // TODO: Use a promise.all to tell Snowflake about all offers at once,
+          // once multiple proxypairs are supported.
+          dbg('Finished gathering ICE candidates.');
+          return snowflake.broker.sendAnswer(this.id, this.pc.localDescription);
+        }
+      };
+      // OnDataChannel triggered remotely from the client when connection succeeds.
+      return this.pc.ondatachannel = (dc) => {
+        var channel;
+        channel = dc.channel;
+        dbg('Data Channel established...');
+        this.prepareDataChannel(channel);
+        return this.client = channel;
+      };
+    }
+
+    receiveWebRTCOffer(offer) {
+      var e, err;
+      if ('offer' !== offer.type) {
+        log('Invalid SDP received -- was not an offer.');
+        return false;
+      }
+      try {
+        err = this.pc.setRemoteDescription(offer);
+      } catch (error) {
+        e = error;
+        log('Invalid SDP message.');
+        return false;
+      }
+      dbg('SDP ' + offer.type + ' successfully received.');
+      return true;
+    }
+
+    prepareDataChannel(channel) {
+      channel.onopen = () => {
+        log('WebRTC DataChannel opened!');
+        snowflake.state = Snowflake.MODE.WEBRTC_READY;
+        snowflake.ui.setActive(true);
+        // This is the point when the WebRTC datachannel is done, so the next step
+        // is to establish websocket to the server.
+        return this.connectRelay();
+      };
+      channel.onclose = () => {
+        log('WebRTC DataChannel closed.');
+        snowflake.ui.setStatus('disconnected by webrtc.');
+        snowflake.ui.setActive(false);
+        snowflake.state = Snowflake.MODE.INIT;
+        this.flush();
+        return this.close();
+      };
+      channel.onerror = function() {
+        return log('Data channel error!');
+      };
+      channel.binaryType = "arraybuffer";
+      return channel.onmessage = this.onClientToRelayMessage;
+    }
+
+    connectRelay() {
+      var params, peer_ip, ref;
+      dbg('Connecting to relay...');
+      // Get a remote IP address from the PeerConnection, if possible. Add it to
+      // the WebSocket URL's query string if available.
+      // MDN marks remoteDescription as "experimental". However the other two
+      // options, currentRemoteDescription and pendingRemoteDescription, which
+      // are not marked experimental, were undefined when I tried them in Firefox
+      // 52.2.0.
+      // https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/remoteDescription
+      peer_ip = Parse.ipFromSDP((ref = this.pc.remoteDescription) != null ? ref.sdp : void 0);
+      params = [];
+      if (peer_ip != null) {
+        params.push(["client_ip", peer_ip]);
+      }
+      this.relay = WS.makeWebsocket(this.relayAddr, params);
+      this.relay.label = 'websocket-relay';
+      this.relay.onopen = () => {
+        if (this.timer) {
+          clearTimeout(this.timer);
+          this.timer = 0;
+        }
+        log(this.relay.label + ' connected!');
+        return snowflake.ui.setStatus('connected');
+      };
+      this.relay.onclose = () => {
+        log(this.relay.label + ' closed.');
+        snowflake.ui.setStatus('disconnected.');
+        snowflake.ui.setActive(false);
+        snowflake.state = Snowflake.MODE.INIT;
+        this.flush();
+        return this.close();
+      };
+      this.relay.onerror = this.onError;
+      this.relay.onmessage = this.onRelayToClientMessage;
+      // TODO: Better websocket timeout handling.
+      return this.timer = setTimeout((() => {
+        if (0 === this.timer) {
+          return;
+        }
+        log(this.relay.label + ' timed out connecting.');
+        return this.relay.onclose();
+      }), 5000);
+    }
+
+    onClientToRelayMessage(msg) {
+      dbg('WebRTC --> websocket data: ' + msg.data.byteLength + ' bytes');
+      this.c2rSchedule.push(msg.data);
+      return this.flush();
+    }
+
+    onRelayToClientMessage(event) {
+      dbg('websocket --> WebRTC data: ' + event.data.byteLength + ' bytes');
+      this.r2cSchedule.push(event.data);
+      return this.flush();
+    }
+
+    onError(event) {
+      var ws;
+      ws = event.target;
+      log(ws.label + ' error.');
+      return this.close();
+    }
+
+    // Close both WebRTC and websocket.
+    close() {
+      var relay;
+      if (this.timer) {
+        clearTimeout(this.timer);
+        this.timer = 0;
+      }
+      this.running = false;
+      if (this.webrtcIsReady()) {
+        this.client.close();
+      }
+      if (this.relayIsReady()) {
+        this.relay.close();
+      }
+      relay = null;
+      return this.onCleanup();
+    }
+
+    flush() {
+      var busy, checkChunks;
+      if (this.flush_timeout_id) {
+        clearTimeout(this.flush_timeout_id);
+      }
+      this.flush_timeout_id = null;
+      busy = true;
+      checkChunks = () => {
+        var chunk;
+        busy = false;
+        // WebRTC --> websocket
+        if (this.relayIsReady() && this.relay.bufferedAmount < this.MAX_BUFFER && this.c2rSchedule.length > 0) {
+          chunk = this.c2rSchedule.shift();
+          this.rateLimit.update(chunk.byteLength);
+          this.relay.send(chunk);
+          busy = true;
+        }
+        // websocket --> WebRTC
+        if (this.webrtcIsReady() && this.client.bufferedAmount < this.MAX_BUFFER && this.r2cSchedule.length > 0) {
+          chunk = this.r2cSchedule.shift();
+          this.rateLimit.update(chunk.byteLength);
+          this.client.send(chunk);
+          return busy = true;
+        }
+      };
+      while (busy && !this.rateLimit.isLimited()) {
+        checkChunks();
+      }
+      if (this.r2cSchedule.length > 0 || this.c2rSchedule.length > 0 || (this.relayIsReady() && this.relay.bufferedAmount > 0) || (this.webrtcIsReady() && this.client.bufferedAmount > 0)) {
+        return this.flush_timeout_id = setTimeout(this.flush, this.rateLimit.when() * 1000);
+      }
+    }
+
+    webrtcIsReady() {
+      return null !== this.client && 'open' === this.client.readyState;
+    }
+
+    relayIsReady() {
+      return (null !== this.relay) && (WebSocket.OPEN === this.relay.readyState);
+    }
+
+    isClosed(ws) {
+      return void 0 === ws || WebSocket.CLOSED === ws.readyState;
+    }
+
+  };
+
+  ProxyPair.prototype.MAX_BUFFER = 10 * 1024 * 1024;
+
+  ProxyPair.prototype.pc = null;
+
+  ProxyPair.prototype.client = null; // WebRTC Data channel
+
+  ProxyPair.prototype.relay = null; // websocket
+
+  ProxyPair.prototype.timer = 0;
+
+  ProxyPair.prototype.running = true;
+
+  ProxyPair.prototype.active = false; // Whether serving a client.
+
+  ProxyPair.prototype.flush_timeout_id = null;
+
+  ProxyPair.prototype.onCleanup = null;
+
+  ProxyPair.prototype.id = null;
+
+  return ProxyPair;
+
+}).call(this);
diff --git a/proxy/shims.coffee b/proxy/shims.coffee
deleted file mode 100644
index 8d3b979..0000000
--- a/proxy/shims.coffee
+++ /dev/null
@@ -1,35 +0,0 @@
-###
-WebRTC shims for multiple browsers.
-###
-
-if module?.exports
-  window = {}
-  document =
-    getElementById: () -> null
-  chrome = {}
-  location = ''
-
-  if not TESTING? or not TESTING
-    webrtc = require 'wrtc'
-
-    PeerConnection = webrtc.RTCPeerConnection
-    IceCandidate = webrtc.RTCIceCandidate
-    SessionDescription = webrtc.RTCSessionDescription
-
-    WebSocket = require 'ws'
-    { XMLHttpRequest } = require 'xmlhttprequest'
-
-else
-  window = this
-  document = window.document
-  chrome = window.chrome
-  location = window.location.search.substr(1)
-
-  PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection ||
-    window.webkitRTCPeerConnection
-  IceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate
-  SessionDescription = window.RTCSessionDescription ||
-    window.mozRTCSessionDescription
-
-  WebSocket = window.WebSocket
-  XMLHttpRequest = window.XMLHttpRequest
diff --git a/proxy/shims.js b/proxy/shims.js
new file mode 100644
index 0000000..0e33236
--- /dev/null
+++ b/proxy/shims.js
@@ -0,0 +1,34 @@
+// Generated by CoffeeScript 2.4.1
+/*
+WebRTC shims for multiple browsers.
+*/
+var IceCandidate, PeerConnection, SessionDescription, WebSocket, XMLHttpRequest, chrome, document, location, webrtc, window;
+
+if (typeof module !== "undefined" && module !== null ? module.exports : void 0) {
+  window = {};
+  document = {
+    getElementById: function() {
+      return null;
+    }
+  };
+  chrome = {};
+  location = '';
+  if ((typeof TESTING === "undefined" || TESTING === null) || !TESTING) {
+    webrtc = require('wrtc');
+    PeerConnection = webrtc.RTCPeerConnection;
+    IceCandidate = webrtc.RTCIceCandidate;
+    SessionDescription = webrtc.RTCSessionDescription;
+    WebSocket = require('ws');
+    ({XMLHttpRequest} = require('xmlhttprequest'));
+  }
+} else {
+  window = this;
+  document = window.document;
+  chrome = window.chrome;
+  location = window.location.search.substr(1);
+  PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
+  IceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate;
+  SessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription;
+  WebSocket = window.WebSocket;
+  XMLHttpRequest = window.XMLHttpRequest;
+}
diff --git a/proxy/snowflake.coffee b/proxy/snowflake.coffee
deleted file mode 100644
index 24e3cf7..0000000
--- a/proxy/snowflake.coffee
+++ /dev/null
@@ -1,132 +0,0 @@
-###
-A Coffeescript WebRTC snowflake proxy
-
-Uses WebRTC from the client, and Websocket to the server.
-
-Assume that the webrtc client plugin is always the offerer, in which case
-this proxy must always act as the answerer.
-
-TODO: More documentation
-###
-
-# Minimum viable snowflake for now - just 1 client.
-class Snowflake
-  relayAddr:  null
-  rateLimit:  null
-  pollInterval: null
-  retries:    0
-
-  # Janky state machine
-  @MODE:
-    INIT:              0
-    WEBRTC_CONNECTING: 1
-    WEBRTC_READY:      2
-
-  @MESSAGE:
-    CONFIRMATION: 'You\'re currently serving a Tor user via Snowflake.'
-
-  # Prepare the Snowflake with a Broker (to find clients) and optional UI.
-  constructor: (@config, @ui, @broker) ->
-    @state = Snowflake.MODE.INIT
-    @proxyPairs = []
-
-    if undefined == @config.rateLimitBytes
-      @rateLimit = new DummyRateLimit()
-    else
-      @rateLimit = new BucketRateLimit(
-        @config.rateLimitBytes * @config.rateLimitHistory,
-        @config.rateLimitHistory
-      )
-    @retries = 0
-
-  # Set the target relay address spec, which is expected to be websocket.
-  # TODO: Should potentially fetch the target from broker later, or modify
-  # entirely for the Tor-independent version.
-  setRelayAddr: (relayAddr) ->
-    @relayAddr = relayAddr
-    log 'Using ' + relayAddr.host + ':' + relayAddr.port + ' as Relay.'
-    return true
-
-  # Initialize WebRTC PeerConnection, which requires beginning the signalling
-  # process. |pollBroker| automatically arranges signalling.
-  beginWebRTC: ->
-    @state = Snowflake.MODE.WEBRTC_CONNECTING
-    log 'ProxyPair Slots: ' + @proxyPairs.length
-    log 'Snowflake IDs: ' + (@proxyPairs.map (p) -> p.id).join ' | '
-    @pollBroker()
-    @pollInterval = setInterval((=> @pollBroker()),
-      @config.defaultBrokerPollInterval)
-
-  # Regularly poll Broker for clients to serve until this snowflake is
-  # serving at capacity, at which point stop polling.
-  pollBroker: ->
-    # Poll broker for clients.
-    pair = @nextAvailableProxyPair()
-    if !pair
-      log 'At client capacity.'
-      # Do nothing until a new proxyPair is available.
-      return
-    pair.active = true
-    msg = 'Polling for client ... '
-    msg += '[retries: ' + @retries + ']' if @retries > 0
-    @ui.setStatus msg
-    recv = @broker.getClientOffer pair.id
-    recv.then (desc) =>
-      if pair.running
-        if !@receiveOffer pair, desc
-          pair.active = false
-      else
-        pair.active = false
-    , (err) ->
-      pair.active = false
-    @retries++
-
-
-  # Returns the first ProxyPair that's available to connect.
-  nextAvailableProxyPair: ->
-    if @proxyPairs.length < @config.connectionsPerClient
-      return @makeProxyPair @relayAddr
-    return @proxyPairs.find (pp, i, arr) -> return !pp.active
-
-  # Receive an SDP offer from some client assigned by the Broker,
-  # |pair| - an available ProxyPair.
-  receiveOffer: (pair, desc) =>
-    try
-      offer = JSON.parse desc
-      dbg 'Received:\n\n' + offer.sdp + '\n'
-      sdp = new SessionDescription offer
-      if pair.receiveWebRTCOffer sdp
-        @sendAnswer pair
-        return true
-      else
-        return false
-    catch e
-      log 'ERROR: Unable to receive Offer: ' + e
-      return false
-
-  sendAnswer: (pair) ->
-    next = (sdp) ->
-      dbg 'webrtc: Answer ready.'
-      pair.pc.setLocalDescription sdp
-    fail = ->
-      dbg 'webrtc: Failed to create Answer'
-    pair.pc.createAnswer()
-    .then next
-    .catch fail
-
-  makeProxyPair: (relay) ->
-    pair = new ProxyPair relay, @rateLimit, @config.pcConfig
-    @proxyPairs.push pair
-    pair.onCleanup = (event) =>
-      # Delete from the list of active proxy pairs.
-      ind = @proxyPairs.indexOf(pair)
-      if ind > -1 then @proxyPairs.splice(ind, 1)
-    pair.begin()
-    return pair
-
-  # Stop all proxypairs.
-  disable: ->
-    log 'Disabling Snowflake.'
-    clearInterval(@pollInterval)
-    while @proxyPairs.length > 0
-      @proxyPairs.pop().close()
diff --git a/proxy/snowflake.js b/proxy/snowflake.js
new file mode 100644
index 0000000..c150452
--- /dev/null
+++ b/proxy/snowflake.js
@@ -0,0 +1,182 @@
+// Generated by CoffeeScript 2.4.1
+/*
+A Coffeescript WebRTC snowflake proxy
+
+Uses WebRTC from the client, and Websocket to the server.
+
+Assume that the webrtc client plugin is always the offerer, in which case
+this proxy must always act as the answerer.
+
+TODO: More documentation
+*/
+var Snowflake;
+
+Snowflake = (function() {
+  // Minimum viable snowflake for now - just 1 client.
+  class Snowflake {
+    // Prepare the Snowflake with a Broker (to find clients) and optional UI.
+    constructor(config, ui, broker) {
+      // Receive an SDP offer from some client assigned by the Broker,
+      // |pair| - an available ProxyPair.
+      this.receiveOffer = this.receiveOffer.bind(this);
+      this.config = config;
+      this.ui = ui;
+      this.broker = broker;
+      this.state = Snowflake.MODE.INIT;
+      this.proxyPairs = [];
+      if (void 0 === this.config.rateLimitBytes) {
+        this.rateLimit = new DummyRateLimit();
+      } else {
+        this.rateLimit = new BucketRateLimit(this.config.rateLimitBytes * this.config.rateLimitHistory, this.config.rateLimitHistory);
+      }
+      this.retries = 0;
+    }
+
+    // Set the target relay address spec, which is expected to be websocket.
+    // TODO: Should potentially fetch the target from broker later, or modify
+    // entirely for the Tor-independent version.
+    setRelayAddr(relayAddr) {
+      this.relayAddr = relayAddr;
+      log('Using ' + relayAddr.host + ':' + relayAddr.port + ' as Relay.');
+      return true;
+    }
+
+    // Initialize WebRTC PeerConnection, which requires beginning the signalling
+    // process. |pollBroker| automatically arranges signalling.
+    beginWebRTC() {
+      this.state = Snowflake.MODE.WEBRTC_CONNECTING;
+      log('ProxyPair Slots: ' + this.proxyPairs.length);
+      log('Snowflake IDs: ' + (this.proxyPairs.map(function(p) {
+        return p.id;
+      })).join(' | '));
+      this.pollBroker();
+      return this.pollInterval = setInterval((() => {
+        return this.pollBroker();
+      }), this.config.defaultBrokerPollInterval);
+    }
+
+    // Regularly poll Broker for clients to serve until this snowflake is
+    // serving at capacity, at which point stop polling.
+    pollBroker() {
+      var msg, pair, recv;
+      // Poll broker for clients.
+      pair = this.nextAvailableProxyPair();
+      if (!pair) {
+        log('At client capacity.');
+        return;
+      }
+      // Do nothing until a new proxyPair is available.
+      pair.active = true;
+      msg = 'Polling for client ... ';
+      if (this.retries > 0) {
+        msg += '[retries: ' + this.retries + ']';
+      }
+      this.ui.setStatus(msg);
+      recv = this.broker.getClientOffer(pair.id);
+      recv.then((desc) => {
+        if (pair.running) {
+          if (!this.receiveOffer(pair, desc)) {
+            return pair.active = false;
+          }
+        } else {
+          return pair.active = false;
+        }
+      }, function(err) {
+        return pair.active = false;
+      });
+      return this.retries++;
+    }
+
+    // Returns the first ProxyPair that's available to connect.
+    nextAvailableProxyPair() {
+      if (this.proxyPairs.length < this.config.connectionsPerClient) {
+        return this.makeProxyPair(this.relayAddr);
+      }
+      return this.proxyPairs.find(function(pp, i, arr) {
+        return !pp.active;
+      });
+    }
+
+    receiveOffer(pair, desc) {
+      var e, offer, sdp;
+      try {
+        offer = JSON.parse(desc);
+        dbg('Received:\n\n' + offer.sdp + '\n');
+        sdp = new SessionDescription(offer);
+        if (pair.receiveWebRTCOffer(sdp)) {
+          this.sendAnswer(pair);
+          return true;
+        } else {
+          return false;
+        }
+      } catch (error) {
+        e = error;
+        log('ERROR: Unable to receive Offer: ' + e);
+        return false;
+      }
+    }
+
+    sendAnswer(pair) {
+      var fail, next;
+      next = function(sdp) {
+        dbg('webrtc: Answer ready.');
+        return pair.pc.setLocalDescription(sdp);
+      };
+      fail = function() {
+        return dbg('webrtc: Failed to create Answer');
+      };
+      return pair.pc.createAnswer().then(next).catch(fail);
+    }
+
+    makeProxyPair(relay) {
+      var pair;
+      pair = new ProxyPair(relay, this.rateLimit, this.config.pcConfig);
+      this.proxyPairs.push(pair);
+      pair.onCleanup = (event) => {
+        var ind;
+        // Delete from the list of active proxy pairs.
+        ind = this.proxyPairs.indexOf(pair);
+        if (ind > -1) {
+          return this.proxyPairs.splice(ind, 1);
+        }
+      };
+      pair.begin();
+      return pair;
+    }
+
+    // Stop all proxypairs.
+    disable() {
+      var results;
+      log('Disabling Snowflake.');
+      clearInterval(this.pollInterval);
+      results = [];
+      while (this.proxyPairs.length > 0) {
+        results.push(this.proxyPairs.pop().close());
+      }
+      return results;
+    }
+
+  };
+
+  Snowflake.prototype.relayAddr = null;
+
+  Snowflake.prototype.rateLimit = null;
+
+  Snowflake.prototype.pollInterval = null;
+
+  Snowflake.prototype.retries = 0;
+
+  // Janky state machine
+  Snowflake.MODE = {
+    INIT: 0,
+    WEBRTC_CONNECTING: 1,
+    WEBRTC_READY: 2
+  };
+
+  Snowflake.MESSAGE = {
+    CONFIRMATION: 'You\'re currently serving a Tor user via Snowflake.'
+  };
+
+  return Snowflake;
+
+}).call(this);
diff --git a/proxy/spec/broker.spec.coffee b/proxy/spec/broker.spec.coffee
deleted file mode 100644
index 2b1d2bd..0000000
--- a/proxy/spec/broker.spec.coffee
+++ /dev/null
@@ -1,92 +0,0 @@
-###
-jasmine tests for Snowflake broker
-###
-
-# fake xhr
-# class XMLHttpRequest
-class XMLHttpRequest
-  constructor: ->
-    @onreadystatechange = null
-  open: ->
-  setRequestHeader: ->
-  send: ->
-  DONE: 1
-
-describe 'Broker', ->
-
-  it 'can be created', ->
-    b = new Broker 'fake'
-    expect(b.url).toEqual 'https://fake/'
-    expect(b.id).not.toBeNull()
-
-  describe 'getClientOffer', ->
-    it 'polls and promises a client offer', (done) ->
-      b = new Broker 'fake'
-      # fake successful request and response from broker.
-      spyOn(b, '_postRequest').and.callFake ->
-        b._xhr.readyState = b._xhr.DONE
-        b._xhr.status = Broker.STATUS.OK
-        b._xhr.responseText = 'fake offer'
-        b._xhr.onreadystatechange()
-      poll = b.getClientOffer()
-      expect(poll).not.toBeNull()
-      expect(b._postRequest).toHaveBeenCalled()
-      poll.then (desc) ->
-        expect(desc).toEqual 'fake offer'
-        done()
-      .catch ->
-        fail 'should not reject on Broker.STATUS.OK'
-        done()
-
-    it 'rejects if the broker timed-out', (done) ->
-      b = new Broker 'fake'
-      # fake timed-out request from broker
-      spyOn(b, '_postRequest').and.callFake ->
-        b._xhr.readyState = b._xhr.DONE
-        b._xhr.status = Broker.STATUS.GATEWAY_TIMEOUT
-        b._xhr.onreadystatechange()
-      poll = b.getClientOffer()
-      expect(poll).not.toBeNull()
-      expect(b._postRequest).toHaveBeenCalled()
-      poll.then (desc) ->
-        fail 'should not fulfill on Broker.STATUS.GATEWAY_TIMEOUT'
-        done()
-      , (err) ->
-        expect(err).toBe Broker.MESSAGE.TIMEOUT
-        done()
-
-    it 'rejects on any other status', (done) ->
-      b = new Broker 'fake'
-      # fake timed-out request from broker
-      spyOn(b, '_postRequest').and.callFake ->
-        b._xhr.readyState = b._xhr.DONE
-        b._xhr.status = 1337
-        b._xhr.onreadystatechange()
-      poll = b.getClientOffer()
-      expect(poll).not.toBeNull()
-      expect(b._postRequest).toHaveBeenCalled()
-      poll.then (desc) ->
-        fail 'should not fulfill on non-OK status'
-        done()
-      , (err) ->
-        expect(err).toBe Broker.MESSAGE.UNEXPECTED
-        expect(b._xhr.status).toBe 1337
-        done()
-
-  it 'responds to the broker with answer', ->
-    b = new Broker 'fake'
-    spyOn(b, '_postRequest')
-    b.sendAnswer 'fake id', 123
-    expect(b._postRequest).toHaveBeenCalledWith(
-      'fake id', jasmine.any(Object), 'answer', '123')
-
-  it 'POST XMLHttpRequests to the broker', ->
-    b = new Broker 'fake'
-    b._xhr = new XMLHttpRequest()
-    spyOn(b._xhr, 'open')
-    spyOn(b._xhr, 'setRequestHeader')
-    spyOn(b._xhr, 'send')
-    b._postRequest 0, b._xhr, 'test', 'data'
-    expect(b._xhr.open).toHaveBeenCalled()
-    expect(b._xhr.setRequestHeader).toHaveBeenCalled()
-    expect(b._xhr.send).toHaveBeenCalled()
diff --git a/proxy/spec/broker.spec.js b/proxy/spec/broker.spec.js
new file mode 100644
index 0000000..7d5e0a2
--- /dev/null
+++ b/proxy/spec/broker.spec.js
@@ -0,0 +1,119 @@
+// Generated by CoffeeScript 2.4.1
+/*
+jasmine tests for Snowflake broker
+*/
+var XMLHttpRequest;
+
+XMLHttpRequest = (function() {
+  // fake xhr
+  // class XMLHttpRequest
+  class XMLHttpRequest {
+    constructor() {
+      this.onreadystatechange = null;
+    }
+
+    open() {}
+
+    setRequestHeader() {}
+
+    send() {}
+
+  };
+
+  XMLHttpRequest.prototype.DONE = 1;
+
+  return XMLHttpRequest;
+
+}).call(this);
+
+describe('Broker', function() {
+  it('can be created', function() {
+    var b;
+    b = new Broker('fake');
+    expect(b.url).toEqual('https://fake/');
+    return expect(b.id).not.toBeNull();
+  });
+  describe('getClientOffer', function() {
+    it('polls and promises a client offer', function(done) {
+      var b, poll;
+      b = new Broker('fake');
+      // fake successful request and response from broker.
+      spyOn(b, '_postRequest').and.callFake(function() {
+        b._xhr.readyState = b._xhr.DONE;
+        b._xhr.status = Broker.STATUS.OK;
+        b._xhr.responseText = 'fake offer';
+        return b._xhr.onreadystatechange();
+      });
+      poll = b.getClientOffer();
+      expect(poll).not.toBeNull();
+      expect(b._postRequest).toHaveBeenCalled();
+      return poll.then(function(desc) {
+        expect(desc).toEqual('fake offer');
+        return done();
+      }).catch(function() {
+        fail('should not reject on Broker.STATUS.OK');
+        return done();
+      });
+    });
+    it('rejects if the broker timed-out', function(done) {
+      var b, poll;
+      b = new Broker('fake');
+      // fake timed-out request from broker
+      spyOn(b, '_postRequest').and.callFake(function() {
+        b._xhr.readyState = b._xhr.DONE;
+        b._xhr.status = Broker.STATUS.GATEWAY_TIMEOUT;
+        return b._xhr.onreadystatechange();
+      });
+      poll = b.getClientOffer();
+      expect(poll).not.toBeNull();
+      expect(b._postRequest).toHaveBeenCalled();
+      return poll.then(function(desc) {
+        fail('should not fulfill on Broker.STATUS.GATEWAY_TIMEOUT');
+        return done();
+      }, function(err) {
+        expect(err).toBe(Broker.MESSAGE.TIMEOUT);
+        return done();
+      });
+    });
+    return it('rejects on any other status', function(done) {
+      var b, poll;
+      b = new Broker('fake');
+      // fake timed-out request from broker
+      spyOn(b, '_postRequest').and.callFake(function() {
+        b._xhr.readyState = b._xhr.DONE;
+        b._xhr.status = 1337;
+        return b._xhr.onreadystatechange();
+      });
+      poll = b.getClientOffer();
+      expect(poll).not.toBeNull();
+      expect(b._postRequest).toHaveBeenCalled();
+      return poll.then(function(desc) {
+        fail('should not fulfill on non-OK status');
+        return done();
+      }, function(err) {
+        expect(err).toBe(Broker.MESSAGE.UNEXPECTED);
+        expect(b._xhr.status).toBe(1337);
+        return done();
+      });
+    });
+  });
+  it('responds to the broker with answer', function() {
+    var b;
+    b = new Broker('fake');
+    spyOn(b, '_postRequest');
+    b.sendAnswer('fake id', 123);
+    return expect(b._postRequest).toHaveBeenCalledWith('fake id', jasmine.any(Object), 'answer', '123');
+  });
+  return it('POST XMLHttpRequests to the broker', function() {
+    var b;
+    b = new Broker('fake');
+    b._xhr = new XMLHttpRequest();
+    spyOn(b._xhr, 'open');
+    spyOn(b._xhr, 'setRequestHeader');
+    spyOn(b._xhr, 'send');
+    b._postRequest(0, b._xhr, 'test', 'data');
+    expect(b._xhr.open).toHaveBeenCalled();
+    expect(b._xhr.setRequestHeader).toHaveBeenCalled();
+    return expect(b._xhr.send).toHaveBeenCalled();
+  });
+});
diff --git a/proxy/spec/init.spec.coffee b/proxy/spec/init.spec.coffee
deleted file mode 100644
index 4134a22..0000000
--- a/proxy/spec/init.spec.coffee
+++ /dev/null
@@ -1,28 +0,0 @@
-
-# Fake snowflake to interact with
-snowflake =
-  ui: new UI
-  broker:
-    sendAnswer: ->
-  state: Snowflake.MODE.INIT
-
-describe 'Init', ->
-
-  it 'gives a dialog when closing, only while active', ->
-    silenceNotifications = false
-    snowflake.state = Snowflake.MODE.WEBRTC_READY
-    msg = window.onbeforeunload()
-    expect(snowflake.state).toBe Snowflake.MODE.WEBRTC_READY
-    expect(msg).toBe Snowflake.MESSAGE.CONFIRMATION
-
-    snowflake.state = Snowflake.MODE.INIT
-    msg = window.onbeforeunload()
-    expect(snowflake.state).toBe Snowflake.MODE.INIT
-    expect(msg).toBe null
-
-  it 'does not give a dialog when silent flag is on', ->
-    silenceNotifications = true
-    snowflake.state = Snowflake.MODE.WEBRTC_READY
-    msg = window.onbeforeunload()
-    expect(snowflake.state).toBe Snowflake.MODE.WEBRTC_READY
-    expect(msg).toBe null
diff --git a/proxy/spec/init.spec.js b/proxy/spec/init.spec.js
new file mode 100644
index 0000000..70ec7e9
--- /dev/null
+++ b/proxy/spec/init.spec.js
@@ -0,0 +1,34 @@
+// Generated by CoffeeScript 2.4.1
+// Fake snowflake to interact with
+var snowflake;
+
+snowflake = {
+  ui: new UI,
+  broker: {
+    sendAnswer: function() {}
+  },
+  state: Snowflake.MODE.INIT
+};
+
+describe('Init', function() {
+  it('gives a dialog when closing, only while active', function() {
+    var msg, silenceNotifications;
+    silenceNotifications = false;
+    snowflake.state = Snowflake.MODE.WEBRTC_READY;
+    msg = window.onbeforeunload();
+    expect(snowflake.state).toBe(Snowflake.MODE.WEBRTC_READY);
+    expect(msg).toBe(Snowflake.MESSAGE.CONFIRMATION);
+    snowflake.state = Snowflake.MODE.INIT;
+    msg = window.onbeforeunload();
+    expect(snowflake.state).toBe(Snowflake.MODE.INIT);
+    return expect(msg).toBe(null);
+  });
+  return it('does not give a dialog when silent flag is on', function() {
+    var msg, silenceNotifications;
+    silenceNotifications = true;
+    snowflake.state = Snowflake.MODE.WEBRTC_READY;
+    msg = window.onbeforeunload();
+    expect(snowflake.state).toBe(Snowflake.MODE.WEBRTC_READY);
+    return expect(msg).toBe(null);
+  });
+});
diff --git a/proxy/spec/proxypair.spec.coffee b/proxy/spec/proxypair.spec.coffee
deleted file mode 100644
index a890780..0000000
--- a/proxy/spec/proxypair.spec.coffee
+++ /dev/null
@@ -1,125 +0,0 @@
-###
-jasmine tests for Snowflake proxypair
-###
-
-# Replacement for MessageEvent constructor.
-# https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/MessageEvent
-MessageEvent = (type, init) ->
-  init
-
-# Asymmetic matcher that checks that two arrays have the same contents.
-arrayMatching = (sample) -> {
-  asymmetricMatch: (other) ->
-    a = new Uint8Array(sample)
-    b = new Uint8Array(other)
-    if a.length != b.length
-      return false
-    for _, i in a
-      if a[i] != b[i]
-        return false
-    true
-  jasmineToString: ->
-    '<arrayMatchine(' + jasmine.pp(sample) + ')>'
-}
-
-describe 'ProxyPair', ->
-  fakeRelay = Parse.address '0.0.0.0:12345'
-  rateLimit = new DummyRateLimit
-  config = new Config
-  destination = []
-  # Using the mock PeerConnection definition from spec/snowflake.spec.coffee.
-  pp = new ProxyPair(fakeRelay, rateLimit, config.pcConfig)
-
-  beforeEach ->
-    pp.begin()
-
-  it 'begins webrtc connection', ->
-    expect(pp.pc).not.toBeNull()
-
-  describe 'accepts WebRTC offer from some client', ->
-    beforeEach ->
-      pp.begin()
-
-    it 'rejects invalid offers', ->
-      expect(typeof(pp.pc.setRemoteDescription)).toBe("function")
-      expect(pp.pc).not.toBeNull()
-      expect(pp.receiveWebRTCOffer {}).toBe false
-      expect(pp.receiveWebRTCOffer {
-        type: 'answer'
-      }).toBe false
-    it 'accepts valid offers', ->
-      expect(pp.pc).not.toBeNull()
-      expect(pp.receiveWebRTCOffer {
-        type: 'offer'
-        sdp: 'foo'
-      }).toBe true
-
-  it 'responds with a WebRTC answer correctly', ->
-    spyOn snowflake.broker, 'sendAnswer'
-    pp.pc.onicecandidate {
-      candidate: null
-    }
-    expect(snowflake.broker.sendAnswer).toHaveBeenCalled()
-
-  it 'handles a new data channel correctly', ->
-    expect(pp.client).toBeNull()
-    pp.pc.ondatachannel {
-      channel: {}
-    }
-    expect(pp.client).not.toBeNull()
-    expect(pp.client.onopen).not.toBeNull()
-    expect(pp.client.onclose).not.toBeNull()
-    expect(pp.client.onerror).not.toBeNull()
-    expect(pp.client.onmessage).not.toBeNull()
-
-  it 'connects to the relay once datachannel opens', ->
-    spyOn pp, 'connectRelay'
-    pp.client.onopen()
-    expect(pp.connectRelay).toHaveBeenCalled()
-
-  it 'connects to a relay', ->
-    pp.connectRelay()
-    expect(pp.relay.onopen).not.toBeNull()
-    expect(pp.relay.onclose).not.toBeNull()
-    expect(pp.relay.onerror).not.toBeNull()
-    expect(pp.relay.onmessage).not.toBeNull()
-
-  describe 'flushes data between client and relay', ->
-
-    it 'proxies data from client to relay', ->
-      pp.pc.ondatachannel {
-        channel: {
-          bufferedAmount: 0
-          readyState: "open"
-          send: (data) ->
-        }
-      }
-      spyOn pp.client, 'send'
-      spyOn pp.relay, 'send'
-      msg = new MessageEvent("message", {
-        data: Uint8Array.from([1, 2, 3]).buffer
-      })
-      pp.onClientToRelayMessage(msg)
-      pp.flush()
-      expect(pp.client.send).not.toHaveBeenCalled()
-      expect(pp.relay.send).toHaveBeenCalledWith arrayMatching([1, 2, 3])
-
-    it 'proxies data from relay to client', ->
-      spyOn pp.client, 'send'
-      spyOn pp.relay, 'send'
-      msg = new MessageEvent("message", {
-        data: Uint8Array.from([4, 5, 6]).buffer
-      })
-      pp.onRelayToClientMessage(msg)
-      pp.flush()
-      expect(pp.client.send).toHaveBeenCalledWith arrayMatching([4, 5, 6])
-      expect(pp.relay.send).not.toHaveBeenCalled()
-
-    it 'sends nothing with nothing to flush', ->
-      spyOn pp.client, 'send'
-      spyOn pp.relay, 'send'
-      pp.flush()
-      expect(pp.client.send).not.toHaveBeenCalled()
-      expect(pp.relay.send).not.toHaveBeenCalled()
-
-# TODO: rate limit tests
diff --git a/proxy/spec/proxypair.spec.js b/proxy/spec/proxypair.spec.js
new file mode 100644
index 0000000..af6a5a6
--- /dev/null
+++ b/proxy/spec/proxypair.spec.js
@@ -0,0 +1,143 @@
+// Generated by CoffeeScript 2.4.1
+/*
+jasmine tests for Snowflake proxypair
+*/
+var MessageEvent, arrayMatching;
+
+// Replacement for MessageEvent constructor.
+// https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/MessageEvent
+MessageEvent = function(type, init) {
+  return init;
+};
+
+// Asymmetic matcher that checks that two arrays have the same contents.
+arrayMatching = function(sample) {
+  return {
+    asymmetricMatch: function(other) {
+      var _, a, b, i, j, len;
+      a = new Uint8Array(sample);
+      b = new Uint8Array(other);
+      if (a.length !== b.length) {
+        return false;
+      }
+      for (i = j = 0, len = a.length; j < len; i = ++j) {
+        _ = a[i];
+        if (a[i] !== b[i]) {
+          return false;
+        }
+      }
+      return true;
+    },
+    jasmineToString: function() {
+      return '<arrayMatchine(' + jasmine.pp(sample) + ')>';
+    }
+  };
+};
+
+describe('ProxyPair', function() {
+  var config, destination, fakeRelay, pp, rateLimit;
+  fakeRelay = Parse.address('0.0.0.0:12345');
+  rateLimit = new DummyRateLimit;
+  config = new Config;
+  destination = [];
+  // Using the mock PeerConnection definition from spec/snowflake.spec.coffee.
+  pp = new ProxyPair(fakeRelay, rateLimit, config.pcConfig);
+  beforeEach(function() {
+    return pp.begin();
+  });
+  it('begins webrtc connection', function() {
+    return expect(pp.pc).not.toBeNull();
+  });
+  describe('accepts WebRTC offer from some client', function() {
+    beforeEach(function() {
+      return pp.begin();
+    });
+    it('rejects invalid offers', function() {
+      expect(typeof pp.pc.setRemoteDescription).toBe("function");
+      expect(pp.pc).not.toBeNull();
+      expect(pp.receiveWebRTCOffer({})).toBe(false);
+      return expect(pp.receiveWebRTCOffer({
+        type: 'answer'
+      })).toBe(false);
+    });
+    return it('accepts valid offers', function() {
+      expect(pp.pc).not.toBeNull();
+      return expect(pp.receiveWebRTCOffer({
+        type: 'offer',
+        sdp: 'foo'
+      })).toBe(true);
+    });
+  });
+  it('responds with a WebRTC answer correctly', function() {
+    spyOn(snowflake.broker, 'sendAnswer');
+    pp.pc.onicecandidate({
+      candidate: null
+    });
+    return expect(snowflake.broker.sendAnswer).toHaveBeenCalled();
+  });
+  it('handles a new data channel correctly', function() {
+    expect(pp.client).toBeNull();
+    pp.pc.ondatachannel({
+      channel: {}
+    });
+    expect(pp.client).not.toBeNull();
+    expect(pp.client.onopen).not.toBeNull();
+    expect(pp.client.onclose).not.toBeNull();
+    expect(pp.client.onerror).not.toBeNull();
+    return expect(pp.client.onmessage).not.toBeNull();
+  });
+  it('connects to the relay once datachannel opens', function() {
+    spyOn(pp, 'connectRelay');
+    pp.client.onopen();
+    return expect(pp.connectRelay).toHaveBeenCalled();
+  });
+  it('connects to a relay', function() {
+    pp.connectRelay();
+    expect(pp.relay.onopen).not.toBeNull();
+    expect(pp.relay.onclose).not.toBeNull();
+    expect(pp.relay.onerror).not.toBeNull();
+    return expect(pp.relay.onmessage).not.toBeNull();
+  });
+  return describe('flushes data between client and relay', function() {
+    it('proxies data from client to relay', function() {
+      var msg;
+      pp.pc.ondatachannel({
+        channel: {
+          bufferedAmount: 0,
+          readyState: "open",
+          send: function(data) {}
+        }
+      });
+      spyOn(pp.client, 'send');
+      spyOn(pp.relay, 'send');
+      msg = new MessageEvent("message", {
+        data: Uint8Array.from([1, 2, 3]).buffer
+      });
+      pp.onClientToRelayMessage(msg);
+      pp.flush();
+      expect(pp.client.send).not.toHaveBeenCalled();
+      return expect(pp.relay.send).toHaveBeenCalledWith(arrayMatching([1, 2, 3]));
+    });
+    it('proxies data from relay to client', function() {
+      var msg;
+      spyOn(pp.client, 'send');
+      spyOn(pp.relay, 'send');
+      msg = new MessageEvent("message", {
+        data: Uint8Array.from([4, 5, 6]).buffer
+      });
+      pp.onRelayToClientMessage(msg);
+      pp.flush();
+      expect(pp.client.send).toHaveBeenCalledWith(arrayMatching([4, 5, 6]));
+      return expect(pp.relay.send).not.toHaveBeenCalled();
+    });
+    return it('sends nothing with nothing to flush', function() {
+      spyOn(pp.client, 'send');
+      spyOn(pp.relay, 'send');
+      pp.flush();
+      expect(pp.client.send).not.toHaveBeenCalled();
+      return expect(pp.relay.send).not.toHaveBeenCalled();
+    });
+  });
+});
+
+// TODO: rate limit tests
diff --git a/proxy/spec/snowflake.spec.coffee b/proxy/spec/snowflake.spec.coffee
deleted file mode 100644
index 2c87204..0000000
--- a/proxy/spec/snowflake.spec.coffee
+++ /dev/null
@@ -1,67 +0,0 @@
-###
-jasmine tests for Snowflake
-###
-
-# Fake browser functionality:
-class PeerConnection
-  setRemoteDescription: ->
-    true
-  send: (data) ->
-class SessionDescription
-  type: 'offer'
-class WebSocket
-  OPEN: 1
-  CLOSED: 0
-  constructor: ->
-    @bufferedAmount = 0
-  send: (data) ->
-
-log = ->
-
-config = new Config
-ui = new UI
-
-class FakeBroker
-  getClientOffer: -> new Promise((F,R) -> {})
-
-describe 'Snowflake', ->
-
-  it 'constructs correctly', ->
-    s = new Snowflake(config, ui, { fake: 'broker' })
-    expect(s.rateLimit).not.toBeNull()
-    expect(s.broker).toEqual { fake: 'broker' }
-    expect(s.ui).not.toBeNull()
-    expect(s.retries).toBe 0
-
-  it 'sets relay address correctly', ->
-    s = new Snowflake(config, ui, null)
-    s.setRelayAddr 'foo'
-    expect(s.relayAddr).toEqual 'foo'
-
-  it 'initalizes WebRTC connection', ->
-    s = new Snowflake(config, ui, new FakeBroker())
-    spyOn(s.broker, 'getClientOffer').and.callThrough()
-    s.beginWebRTC()
-    expect(s.retries).toBe 1
-    expect(s.broker.getClientOffer).toHaveBeenCalled()
-
-  it 'receives SDP offer and sends answer', ->
-    s = new Snowflake(config, ui, new FakeBroker())
-    pair = { receiveWebRTCOffer: -> }
-    spyOn(pair, 'receiveWebRTCOffer').and.returnValue true
-    spyOn(s, 'sendAnswer')
-    s.receiveOffer pair, '{"type":"offer","sdp":"foo"}'
-    expect(s.sendAnswer).toHaveBeenCalled()
-
-  it 'does not send answer when receiving invalid offer', ->
-    s = new Snowflake(config, ui, new FakeBroker())
-    pair = { receiveWebRTCOffer: -> }
-    spyOn(pair, 'receiveWebRTCOffer').and.returnValue false
-    spyOn(s, 'sendAnswer')
-    s.receiveOffer pair, '{"type":"not a good offer","sdp":"foo"}'
-    expect(s.sendAnswer).not.toHaveBeenCalled()
-
-  it 'can make a proxypair', ->
-    s = new Snowflake(config, ui, new FakeBroker())
-    s.makeProxyPair()
-    expect(s.proxyPairs.length).toBe 1
diff --git a/proxy/spec/snowflake.spec.js b/proxy/spec/snowflake.spec.js
new file mode 100644
index 0000000..d3fb988
--- /dev/null
+++ b/proxy/spec/snowflake.spec.js
@@ -0,0 +1,114 @@
+// Generated by CoffeeScript 2.4.1
+/*
+jasmine tests for Snowflake
+*/
+var FakeBroker, PeerConnection, SessionDescription, WebSocket, config, log, ui;
+
+// Fake browser functionality:
+PeerConnection = class PeerConnection {
+  setRemoteDescription() {
+    return true;
+  }
+
+  send(data) {}
+
+};
+
+SessionDescription = (function() {
+  class SessionDescription {};
+
+  SessionDescription.prototype.type = 'offer';
+
+  return SessionDescription;
+
+}).call(this);
+
+WebSocket = (function() {
+  class WebSocket {
+    constructor() {
+      this.bufferedAmount = 0;
+    }
+
+    send(data) {}
+
+  };
+
+  WebSocket.prototype.OPEN = 1;
+
+  WebSocket.prototype.CLOSED = 0;
+
+  return WebSocket;
+
+}).call(this);
+
+log = function() {};
+
+config = new Config;
+
+ui = new UI;
+
+FakeBroker = class FakeBroker {
+  getClientOffer() {
+    return new Promise(function(F, R) {
+      return {};
+    });
+  }
+
+};
+
+describe('Snowflake', function() {
+  it('constructs correctly', function() {
+    var s;
+    s = new Snowflake(config, ui, {
+      fake: 'broker'
+    });
+    expect(s.rateLimit).not.toBeNull();
+    expect(s.broker).toEqual({
+      fake: 'broker'
+    });
+    expect(s.ui).not.toBeNull();
+    return expect(s.retries).toBe(0);
+  });
+  it('sets relay address correctly', function() {
+    var s;
+    s = new Snowflake(config, ui, null);
+    s.setRelayAddr('foo');
+    return expect(s.relayAddr).toEqual('foo');
+  });
+  it('initalizes WebRTC connection', function() {
+    var s;
+    s = new Snowflake(config, ui, new FakeBroker());
+    spyOn(s.broker, 'getClientOffer').and.callThrough();
+    s.beginWebRTC();
+    expect(s.retries).toBe(1);
+    return expect(s.broker.getClientOffer).toHaveBeenCalled();
+  });
+  it('receives SDP offer and sends answer', function() {
+    var pair, s;
+    s = new Snowflake(config, ui, new FakeBroker());
+    pair = {
+      receiveWebRTCOffer: function() {}
+    };
+    spyOn(pair, 'receiveWebRTCOffer').and.returnValue(true);
+    spyOn(s, 'sendAnswer');
+    s.receiveOffer(pair, '{"type":"offer","sdp":"foo"}');
+    return expect(s.sendAnswer).toHaveBeenCalled();
+  });
+  it('does not send answer when receiving invalid offer', function() {
+    var pair, s;
+    s = new Snowflake(config, ui, new FakeBroker());
+    pair = {
+      receiveWebRTCOffer: function() {}
+    };
+    spyOn(pair, 'receiveWebRTCOffer').and.returnValue(false);
+    spyOn(s, 'sendAnswer');
+    s.receiveOffer(pair, '{"type":"not a good offer","sdp":"foo"}');
+    return expect(s.sendAnswer).not.toHaveBeenCalled();
+  });
+  return it('can make a proxypair', function() {
+    var s;
+    s = new Snowflake(config, ui, new FakeBroker());
+    s.makeProxyPair();
+    return expect(s.proxyPairs.length).toBe(1);
+  });
+});
diff --git a/proxy/spec/ui.spec.coffee b/proxy/spec/ui.spec.coffee
deleted file mode 100644
index 8769b0c..0000000
--- a/proxy/spec/ui.spec.coffee
+++ /dev/null
@@ -1,57 +0,0 @@
-###
-jasmine tests for Snowflake UI
-###
-
-document =
-  getElementById: (id) -> {}
-  createTextNode: (txt) -> txt
-
-describe 'UI', ->
-
-  it 'activates debug mode when badge does not exist', ->
-    spyOn(document, 'getElementById').and.callFake (id) ->
-      return null if 'badge' == id
-      return {}
-    u = new DebugUI()
-    expect(document.getElementById.calls.count()).toEqual 2
-    expect(u.$status).not.toBeNull()
-    expect(u.$msglog).not.toBeNull()
-
-  it 'is not debug mode when badge exists', ->
-    spyOn(document, 'getElementById').and.callFake (id) ->
-      return {} if 'badge' == id
-      return null
-    u = new BadgeUI()
-    expect(document.getElementById).toHaveBeenCalled()
-    expect(document.getElementById.calls.count()).toEqual 1
-    expect(u.$badge).not.toBeNull()
-
-  it 'sets status message when in debug mode', ->
-    u = new DebugUI()
-    u.$status =
-      innerHTML: ''
-      appendChild: (txt) -> @innerHTML = txt
-    u.setStatus('test')
-    expect(u.$status.innerHTML).toEqual 'Status: test'
-
-  it 'sets message log css correctly for debug mode', ->
-    u = new DebugUI()
-    u.setActive true
-    expect(u.$msglog.className).toEqual 'active'
-    u.setActive false
-    expect(u.$msglog.className).toEqual ''
-
-  it 'sets badge css correctly for non-debug mode', ->
-    u = new BadgeUI()
-    u.$badge = {}
-    u.setActive true
-    expect(u.$badge.className).toEqual 'active'
-    u.setActive false
-    expect(u.$badge.className).toEqual ''
-
-  it 'logs to the textarea correctly when debug mode', ->
-    u = new DebugUI()
-    u.$msglog = { value: '', scrollTop: 0, scrollHeight: 1337 }
-    u.log 'test'
-    expect(u.$msglog.value).toEqual 'test\n'
-    expect(u.$msglog.scrollTop).toEqual 1337
diff --git a/proxy/spec/ui.spec.js b/proxy/spec/ui.spec.js
new file mode 100644
index 0000000..380f41a
--- /dev/null
+++ b/proxy/spec/ui.spec.js
@@ -0,0 +1,84 @@
+// Generated by CoffeeScript 2.4.1
+/*
+jasmine tests for Snowflake UI
+*/
+var document;
+
+document = {
+  getElementById: function(id) {
+    return {};
+  },
+  createTextNode: function(txt) {
+    return txt;
+  }
+};
+
+describe('UI', function() {
+  it('activates debug mode when badge does not exist', function() {
+    var u;
+    spyOn(document, 'getElementById').and.callFake(function(id) {
+      if ('badge' === id) {
+        return null;
+      }
+      return {};
+    });
+    u = new DebugUI();
+    expect(document.getElementById.calls.count()).toEqual(2);
+    expect(u.$status).not.toBeNull();
+    return expect(u.$msglog).not.toBeNull();
+  });
+  it('is not debug mode when badge exists', function() {
+    var u;
+    spyOn(document, 'getElementById').and.callFake(function(id) {
+      if ('badge' === id) {
+        return {};
+      }
+      return null;
+    });
+    u = new BadgeUI();
+    expect(document.getElementById).toHaveBeenCalled();
+    expect(document.getElementById.calls.count()).toEqual(1);
+    return expect(u.$badge).not.toBeNull();
+  });
+  it('sets status message when in debug mode', function() {
+    var u;
+    u = new DebugUI();
+    u.$status = {
+      innerHTML: '',
+      appendChild: function(txt) {
+        return this.innerHTML = txt;
+      }
+    };
+    u.setStatus('test');
+    return expect(u.$status.innerHTML).toEqual('Status: test');
+  });
+  it('sets message log css correctly for debug mode', function() {
+    var u;
+    u = new DebugUI();
+    u.setActive(true);
+    expect(u.$msglog.className).toEqual('active');
+    u.setActive(false);
+    return expect(u.$msglog.className).toEqual('');
+  });
+  it('sets badge css correctly for non-debug mode', function() {
+    var u;
+    u = new BadgeUI();
+    u.$badge = {};
+    u.setActive(true);
+    expect(u.$badge.className).toEqual('active');
+    u.setActive(false);
+    return expect(u.$badge.className).toEqual('');
+  });
+  return it('logs to the textarea correctly when debug mode', function() {
+    var u;
+    u = new DebugUI();
+    u.$msglog = {
+      value: '',
+      scrollTop: 0,
+      scrollHeight: 1337
+    };
+    u.log('test');
+    expect(u.$msglog.value).toEqual('test\n');
+    return expect(u.$msglog.scrollTop).toEqual(1337);
+  });
+});
diff --git a/proxy/spec/util.spec.coffee b/proxy/spec/util.spec.coffee
deleted file mode 100644
index 88b67b2..0000000
--- a/proxy/spec/util.spec.coffee
+++ /dev/null
@@ -1,236 +0,0 @@
-###
-jasmine tests for Snowflake utils
-###
-
-describe 'Parse', ->
-
-  describe 'cookie', ->
-    it 'parses correctly', ->
-      expect Parse.cookie ''
-        .toEqual {}
-      expect Parse.cookie 'a=b'
-        .toEqual { a: 'b' }
-      expect Parse.cookie 'a=b=c'
-        .toEqual { a: 'b=c' }
-      expect Parse.cookie 'a=b; c=d'
-        .toEqual { a: 'b', c: 'd' }
-      expect Parse.cookie 'a=b ; c=d'
-        .toEqual { a: 'b', c: 'd' }
-      expect Parse.cookie 'a= b'
-        .toEqual { a: 'b' }
-      expect Parse.cookie 'a='
-        .toEqual { a: '' }
-      expect Parse.cookie 'key'
-        .toBeNull()
-      expect Parse.cookie 'key=%26%20'
-        .toEqual { key: '& ' }
-      expect Parse.cookie 'a=\'\''
-        .toEqual { a: '\'\'' }
-
-  describe 'address', ->
-    it 'parses IPv4', ->
-      expect Parse.address ''
-        .toBeNull()
-      expect Parse.address '3.3.3.3:4444'
-        .toEqual { host: '3.3.3.3', port: 4444 }
-      expect Parse.address '3.3.3.3'
-        .toBeNull()
-      expect Parse.address '3.3.3.3:0x1111'
-        .toBeNull()
-      expect Parse.address '3.3.3.3:-4444'
-        .toBeNull()
-      expect Parse.address '3.3.3.3:65536'
-        .toBeNull()
-    it 'parses IPv6', ->
-      expect Parse.address '[1:2::a:f]:4444'
-        .toEqual { host: '1:2::a:f', port: 4444 }
-      expect Parse.address '[1:2::a:f]'
-        .toBeNull()
-      expect Parse.address '[1:2::a:f]:0x1111'
-        .toBeNull()
-      expect Parse.address '[1:2::a:f]:-4444'
-        .toBeNull()
-      expect Parse.address '[1:2::a:f]:65536'
-        .toBeNull()
-      expect Parse.address '[1:2::ffff:1.2.3.4]:4444'
-        .toEqual { host: '1:2::ffff:1.2.3.4', port: 4444 }
-
-  describe 'ipFromSDP', ->
-    testCases = [
-      # https://tools.ietf.org/html/rfc4566#section-5
-      sdp: """
-           v=0
-           o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5
-           s=SDP Seminar
-           i=A Seminar on the session description protocol
-           u=http://www.example.com/seminars/sdp.pdf
-           e=j.doe at example.com (Jane Doe)
-           c=IN IP4 224.2.17.12/127
-           t=2873397496 2873404696
-           a=recvonly
-           m=audio 49170 RTP/AVP 0
-           m=video 51372 RTP/AVP 99
-           a=rtpmap:99 h263-1998/90000
-           """
-      expected: '224.2.17.12'
-    ,
-      # Missing c= line
-      sdp: """
-           v=0
-           o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5
-           s=SDP Seminar
-           i=A Seminar on the session description protocol
-           u=http://www.example.com/seminars/sdp.pdf
-           e=j.doe at example.com (Jane Doe)
-           t=2873397496 2873404696
-           a=recvonly
-           m=audio 49170 RTP/AVP 0
-           m=video 51372 RTP/AVP 99
-           a=rtpmap:99 h263-1998/90000
-           """
-      expected: undefined
-    ,
-      # Single line, IP address only
-      sdp: "c=IN IP4 224.2.1.1\n"
-      expected: '224.2.1.1'
-    ,
-      # Same, with TTL
-      sdp: "c=IN IP4 224.2.1.1/127\n"
-      expected: '224.2.1.1'
-    ,
-      # Same, with TTL and multicast addresses
-      sdp: "c=IN IP4 224.2.1.1/127/3\n"
-      expected: '224.2.1.1'
-    ,
-      # IPv6, address only
-      sdp: "c=IN IP6 FF15::101\n"
-      expected: 'ff15::101'
-    ,
-      # Same, with multicast addresses
-      sdp: "c=IN IP6 FF15::101/3\n"
-      expected: 'ff15::101'
-    ,
-      # Multiple c= lines
-      sdp: """
-           c=IN IP4 1.2.3.4
-           c=IN IP4 5.6.7.8
-           """
-      expected: '1.2.3.4'
-    ,
-      # Modified from SDP sent by snowflake-client.
-      # coffeelint: disable
-      sdp: """
-           v=0
-           o=- 7860378660295630295 2 IN IP4 127.0.0.1
-           s=-
-           t=0 0
-           a=group:BUNDLE data
-           a=msid-semantic: WMS
-           m=application 54653 DTLS/SCTP 5000
-           c=IN IP4 1.2.3.4
-           a=candidate:3581707038 1 udp 2122260223 192.168.0.1 54653 typ host generation 0 network-id 1 network-cost 50
-           a=candidate:2617212910 1 tcp 1518280447 192.168.0.1 59673 typ host tcptype passive generation 0 network-id 1 network-cost 50
-           a=candidate:2082671819 1 udp 1686052607 1.2.3.4 54653 typ srflx raddr 192.168.0.1 rport 54653 generation 0 network-id 1 network-cost 50
-           a=ice-ufrag:IBdf
-           a=ice-pwd:G3lTrrC9gmhQx481AowtkhYz
-           a=fingerprint:sha-256 53:F8:84:D9:3C:1F:A0:44:AA:D6:3C:65:80:D3:CB:6F:23:90:17:41:06:F9:9C:10:D8:48:4A:A8:B6:FA:14:A1
-           a=setup:actpass
-           a=mid:data
-           a=sctpmap:5000 webrtc-datachannel 1024
-           """
-      # coffeelint: enable
-      expected: '1.2.3.4'
-    ,
-      # Improper character within IPv4
-      sdp: """
-           c=IN IP4 224.2z.1.1
-           """
-      expected: undefined
-    ,
-      # Improper character within IPv6
-      sdp: """
-           c=IN IP6 ff15:g::101
-           """
-      expected: undefined
-    ,
-      # Bogus "IP7" addrtype
-      sdp: "c=IN IP7 1.2.3.4\n"
-      expected: undefined
-    ]
-
-    it 'parses SDP', ->
-      for test in testCases
-        # https://tools.ietf.org/html/rfc4566#section-5: "The sequence # CRLF
-        # (0x0d0a) is used to end a record, although parsers SHOULD be tolerant
-        # and also accept records terminated with a single newline character."
-        # We represent the test cases with LF line endings for convenience, and
-        # test them both that way and with CRLF line endings.
-        expect(Parse.ipFromSDP(test.sdp)?.toLowerCase()).toEqual(test.expected)
-        expect(
-          Parse.ipFromSDP(test.sdp.replace(/\n/, "\r\n"))?.toLowerCase()
-        ).toEqual(test.expected)
-
-describe 'query string', ->
-  it 'should parse correctly', ->
-    expect Query.parse ''
-      .toEqual {}
-    expect Query.parse 'a=b'
-      .toEqual { a: 'b' }
-    expect Query.parse 'a=b=c'
-      .toEqual { a: 'b=c' }
-    expect Query.parse 'a=b&c=d'
-      .toEqual { a: 'b', c: 'd' }
-    expect Query.parse 'client=&relay=1.2.3.4%3A9001'
-      .toEqual { client: '', relay: '1.2.3.4:9001' }
-    expect Query.parse 'a=b%26c=d'
-      .toEqual { a: 'b&c=d' }
-    expect Query.parse 'a%3db=d'
-      .toEqual { 'a=b': 'd' }
-    expect Query.parse 'a=b+c%20d'
-      .toEqual { 'a': 'b c d' }
-    expect Query.parse 'a=b+c%2bd'
-      .toEqual { 'a': 'b c+d' }
-    expect Query.parse 'a+b=c'
-      .toEqual { 'a b': 'c' }
-    expect Query.parse 'a=b+c+d'
-      .toEqual { a: 'b c d' }
-  it 'uses the first appearance of duplicate key', ->
-    expect Query.parse 'a=b&c=d&a=e'
-      .toEqual { a: 'b', c: 'd' }
-    expect Query.parse 'a'
-      .toEqual { a: '' }
-    expect Query.parse '=b'
-      .toEqual { '': 'b' }
-    expect Query.parse '&a=b'
-      .toEqual { '': '', a: 'b' }
-    expect Query.parse 'a=b&'
-      .toEqual { a: 'b', '':'' }
-    expect Query.parse 'a=b&&c=d'
-      .toEqual { a: 'b', '':'', c: 'd' }
-
-describe 'Params', ->
-  describe 'bool', ->
-    getBool = (query) ->
-      Params.getBool (Query.parse query), 'param', false
-    it 'parses correctly', ->
-      expect(getBool 'param=true').toBe true
-      expect(getBool 'param').toBe true
-      expect(getBool 'param=').toBe true
-      expect(getBool 'param=1').toBe true
-      expect(getBool 'param=0').toBe false
-      expect(getBool 'param=false').toBe false
-      expect(getBool 'param=unexpected').toBeNull()
-      expect(getBool 'pram=true').toBe false
-
-  describe 'address', ->
-    DEFAULT = { host: '1.1.1.1', port: 2222 }
-    getAddress = (query) ->
-      Params.getAddress query, 'addr', DEFAULT
-    it 'parses correctly', ->
-      expect(getAddress {}).toEqual DEFAULT
-      expect getAddress { addr: '3.3.3.3:4444' }
-        .toEqual { host: '3.3.3.3', port: 4444 }
-      expect getAddress { x: '3.3.3.3:4444' }
-        .toEqual DEFAULT
-      expect getAddress { addr: '---' }
-        .toBeNull()
diff --git a/proxy/spec/util.spec.js b/proxy/spec/util.spec.js
new file mode 100644
index 0000000..65f2324
--- /dev/null
+++ b/proxy/spec/util.spec.js
@@ -0,0 +1,254 @@
+// Generated by CoffeeScript 2.4.1
+/*
+jasmine tests for Snowflake utils
+*/
+describe('Parse', function() {
+  describe('cookie', function() {
+    return it('parses correctly', function() {
+      expect(Parse.cookie('')).toEqual({});
+      expect(Parse.cookie('a=b')).toEqual({
+        a: 'b'
+      });
+      expect(Parse.cookie('a=b=c')).toEqual({
+        a: 'b=c'
+      });
+      expect(Parse.cookie('a=b; c=d')).toEqual({
+        a: 'b',
+        c: 'd'
+      });
+      expect(Parse.cookie('a=b ; c=d')).toEqual({
+        a: 'b',
+        c: 'd'
+      });
+      expect(Parse.cookie('a= b')).toEqual({
+        a: 'b'
+      });
+      expect(Parse.cookie('a=')).toEqual({
+        a: ''
+      });
+      expect(Parse.cookie('key')).toBeNull();
+      expect(Parse.cookie('key=%26%20')).toEqual({
+        key: '& '
+      });
+      return expect(Parse.cookie('a=\'\'')).toEqual({
+        a: '\'\''
+      });
+    });
+  });
+  describe('address', function() {
+    it('parses IPv4', function() {
+      expect(Parse.address('')).toBeNull();
+      expect(Parse.address('3.3.3.3:4444')).toEqual({
+        host: '3.3.3.3',
+        port: 4444
+      });
+      expect(Parse.address('3.3.3.3')).toBeNull();
+      expect(Parse.address('3.3.3.3:0x1111')).toBeNull();
+      expect(Parse.address('3.3.3.3:-4444')).toBeNull();
+      return expect(Parse.address('3.3.3.3:65536')).toBeNull();
+    });
+    return it('parses IPv6', function() {
+      expect(Parse.address('[1:2::a:f]:4444')).toEqual({
+        host: '1:2::a:f',
+        port: 4444
+      });
+      expect(Parse.address('[1:2::a:f]')).toBeNull();
+      expect(Parse.address('[1:2::a:f]:0x1111')).toBeNull();
+      expect(Parse.address('[1:2::a:f]:-4444')).toBeNull();
+      expect(Parse.address('[1:2::a:f]:65536')).toBeNull();
+      return expect(Parse.address('[1:2::ffff:1.2.3.4]:4444')).toEqual({
+        host: '1:2::ffff:1.2.3.4',
+        port: 4444
+      });
+    });
+  });
+  return describe('ipFromSDP', function() {
+    var testCases;
+    testCases = [
+      {
+        // https://tools.ietf.org/html/rfc4566#section-5
+        sdp: "v=0\no=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\ns=SDP Seminar\ni=A Seminar on the session description protocol\nu=http://www.example.com/seminars/sdp.pdf\ne=j.doe@example.com (Jane Doe)\nc=IN IP4 224.2.17.12/127\nt=2873397496 2873404696\na=recvonly\nm=audio 49170 RTP/AVP 0\nm=video 51372 RTP/AVP 99\na=rtpmap:99 h263-1998/90000",
+        expected: '224.2.17.12'
+      },
+      {
+        // Missing c= line
+        sdp: "v=0\no=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\ns=SDP Seminar\ni=A Seminar on the session description protocol\nu=http://www.example.com/seminars/sdp.pdf\ne=j.doe@example.com (Jane Doe)\nt=2873397496 2873404696\na=recvonly\nm=audio 49170 RTP/AVP 0\nm=video 51372 RTP/AVP 99\na=rtpmap:99 h263-1998/90000",
+        expected: void 0
+      },
+      {
+        // Single line, IP address only
+        sdp: "c=IN IP4 224.2.1.1\n",
+        expected: '224.2.1.1'
+      },
+      {
+        // Same, with TTL
+        sdp: "c=IN IP4 224.2.1.1/127\n",
+        expected: '224.2.1.1'
+      },
+      {
+        // Same, with TTL and multicast addresses
+        sdp: "c=IN IP4 224.2.1.1/127/3\n",
+        expected: '224.2.1.1'
+      },
+      {
+        // IPv6, address only
+        sdp: "c=IN IP6 FF15::101\n",
+        expected: 'ff15::101'
+      },
+      {
+        // Same, with multicast addresses
+        sdp: "c=IN IP6 FF15::101/3\n",
+        expected: 'ff15::101'
+      },
+      {
+        // Multiple c= lines
+        sdp: "c=IN IP4 1.2.3.4\nc=IN IP4 5.6.7.8",
+        expected: '1.2.3.4'
+      },
+      {
+        // Modified from SDP sent by snowflake-client.
+        // coffeelint: disable
+        sdp: "v=0\no=- 7860378660295630295 2 IN IP4 127.0.0.1\ns=-\nt=0 0\na=group:BUNDLE data\na=msid-semantic: WMS\nm=application 54653 DTLS/SCTP 5000\nc=IN IP4 1.2.3.4\na=candidate:3581707038 1 udp 2122260223 192.168.0.1 54653 typ host generation 0 network-id 1 network-cost 50\na=candidate:2617212910 1 tcp 1518280447 192.168.0.1 59673 typ host tcptype passive generation 0 network-id 1 network-cost 50\na=candidate:2082671819 1 udp 1686052607 1.2.3.4 54653 typ srflx raddr 192.168.0.1 rport 54653 generation 0 network-id 1 network-cost 50\na=ice-ufrag:IBdf\na=ice-pwd:G3lTrrC9gmhQx481AowtkhYz\na=fingerprint:sha-256 53:F8:84:D9:3C:1F:A0:44:AA:D6:3C:65:80:D3:CB:6F:23:90:17:41:06:F9:9C:10:D8:48:4A:A8:B6:FA:14:A1\na=setup:actpass\na=mid:data\na=sctpmap:5000 webrtc-datachannel 1024",
+        // coffeelint: enable
+        expected: '1.2.3.4'
+      },
+      {
+        // Improper character within IPv4
+        sdp: "c=IN IP4 224.2z.1.1",
+        expected: void 0
+      },
+      {
+        // Improper character within IPv6
+        sdp: "c=IN IP6 ff15:g::101",
+        expected: void 0
+      },
+      {
+        // Bogus "IP7" addrtype
+        sdp: "c=IN IP7 1.2.3.4\n",
+        expected: void 0
+      }
+    ];
+    return it('parses SDP', function() {
+      var i, len, ref, ref1, results, test;
+      results = [];
+      for (i = 0, len = testCases.length; i < len; i++) {
+        test = testCases[i];
+        // https://tools.ietf.org/html/rfc4566#section-5: "The sequence # CRLF
+        // (0x0d0a) is used to end a record, although parsers SHOULD be tolerant
+        // and also accept records terminated with a single newline character."
+        // We represent the test cases with LF line endings for convenience, and
+        // test them both that way and with CRLF line endings.
+        expect((ref = Parse.ipFromSDP(test.sdp)) != null ? ref.toLowerCase() : void 0).toEqual(test.expected);
+        results.push(expect((ref1 = Parse.ipFromSDP(test.sdp.replace(/\n/, "\r\n"))) != null ? ref1.toLowerCase() : void 0).toEqual(test.expected));
+      }
+      return results;
+    });
+  });
+});
+
+describe('query string', function() {
+  it('should parse correctly', function() {
+    expect(Query.parse('')).toEqual({});
+    expect(Query.parse('a=b')).toEqual({
+      a: 'b'
+    });
+    expect(Query.parse('a=b=c')).toEqual({
+      a: 'b=c'
+    });
+    expect(Query.parse('a=b&c=d')).toEqual({
+      a: 'b',
+      c: 'd'
+    });
+    expect(Query.parse('client=&relay=1.2.3.4%3A9001')).toEqual({
+      client: '',
+      relay: '1.2.3.4:9001'
+    });
+    expect(Query.parse('a=b%26c=d')).toEqual({
+      a: 'b&c=d'
+    });
+    expect(Query.parse('a%3db=d')).toEqual({
+      'a=b': 'd'
+    });
+    expect(Query.parse('a=b+c%20d')).toEqual({
+      'a': 'b c d'
+    });
+    expect(Query.parse('a=b+c%2bd')).toEqual({
+      'a': 'b c+d'
+    });
+    expect(Query.parse('a+b=c')).toEqual({
+      'a b': 'c'
+    });
+    return expect(Query.parse('a=b+c+d')).toEqual({
+      a: 'b c d'
+    });
+  });
+  return it('uses the first appearance of duplicate key', function() {
+    expect(Query.parse('a=b&c=d&a=e')).toEqual({
+      a: 'b',
+      c: 'd'
+    });
+    expect(Query.parse('a')).toEqual({
+      a: ''
+    });
+    expect(Query.parse('=b')).toEqual({
+      '': 'b'
+    });
+    expect(Query.parse('&a=b')).toEqual({
+      '': '',
+      a: 'b'
+    });
+    expect(Query.parse('a=b&')).toEqual({
+      a: 'b',
+      '': ''
+    });
+    return expect(Query.parse('a=b&&c=d')).toEqual({
+      a: 'b',
+      '': '',
+      c: 'd'
+    });
+  });
+});
+
+describe('Params', function() {
+  describe('bool', function() {
+    var getBool;
+    getBool = function(query) {
+      return Params.getBool(Query.parse(query), 'param', false);
+    };
+    return it('parses correctly', function() {
+      expect(getBool('param=true')).toBe(true);
+      expect(getBool('param')).toBe(true);
+      expect(getBool('param=')).toBe(true);
+      expect(getBool('param=1')).toBe(true);
+      expect(getBool('param=0')).toBe(false);
+      expect(getBool('param=false')).toBe(false);
+      expect(getBool('param=unexpected')).toBeNull();
+      return expect(getBool('pram=true')).toBe(false);
+    });
+  });
+  return describe('address', function() {
+    var DEFAULT, getAddress;
+    DEFAULT = {
+      host: '1.1.1.1',
+      port: 2222
+    };
+    getAddress = function(query) {
+      return Params.getAddress(query, 'addr', DEFAULT);
+    };
+    return it('parses correctly', function() {
+      expect(getAddress({})).toEqual(DEFAULT);
+      expect(getAddress({
+        addr: '3.3.3.3:4444'
+      })).toEqual({
+        host: '3.3.3.3',
+        port: 4444
+      });
+      expect(getAddress({
+        x: '3.3.3.3:4444'
+      })).toEqual(DEFAULT);
+      return expect(getAddress({
+        addr: '---'
+      })).toBeNull();
+    });
+  });
+});
diff --git a/proxy/spec/websocket.spec.coffee b/proxy/spec/websocket.spec.coffee
deleted file mode 100644
index 3818e2a..0000000
--- a/proxy/spec/websocket.spec.coffee
+++ /dev/null
@@ -1,39 +0,0 @@
-###
-jasmine tests for Snowflake websocket
-###
-
-describe 'BuildUrl', ->
-  it 'should parse just protocol and host', ->
-    expect(WS.buildUrl('http', 'example.com')).toBe 'http://example.com'
-  it 'should handle different ports', ->
-    expect WS.buildUrl 'http', 'example.com', 80
-      .toBe 'http://example.com'
-    expect WS.buildUrl 'http', 'example.com', 81
-      .toBe 'http://example.com:81'
-    expect WS.buildUrl 'http', 'example.com', 443
-      .toBe 'http://example.com:443'
-    expect WS.buildUrl 'http', 'example.com', 444
-      .toBe 'http://example.com:444'
-  it 'should handle paths', ->
-    expect WS.buildUrl 'http', 'example.com', 80, '/'
-      .toBe 'http://example.com/'
-    expect WS.buildUrl 'http', 'example.com', 80,'/test?k=%#v'
-      .toBe 'http://example.com/test%3Fk%3D%25%23v'
-    expect WS.buildUrl 'http', 'example.com', 80, '/test'
-      .toBe 'http://example.com/test'
-  it 'should handle params', ->
-    expect WS.buildUrl 'http', 'example.com', 80, '/test', [['k', '%#v']]
-      .toBe 'http://example.com/test?k=%25%23v'
-    expect(WS.buildUrl(
-      'http', 'example.com', 80, '/test', [['a', 'b'], ['c', 'd']]
-    )).toBe 'http://example.com/test?a=b&c=d'
-  it 'should handle ips', ->
-    expect WS.buildUrl 'http', '1.2.3.4'
-      .toBe 'http://1.2.3.4'
-    expect WS.buildUrl 'http', '1:2::3:4'
-      .toBe 'http://[1:2::3:4]'
-  it 'should handle bogus', ->
-    expect WS.buildUrl 'http', 'bog][us'
-      .toBe 'http://bog%5D%5Bus'
-    expect WS.buildUrl 'http', 'bog:u]s'
-      .toBe 'http://bog%3Au%5Ds'
diff --git a/proxy/spec/websocket.spec.js b/proxy/spec/websocket.spec.js
new file mode 100644
index 0000000..41314df
--- /dev/null
+++ b/proxy/spec/websocket.spec.js
@@ -0,0 +1,32 @@
+// Generated by CoffeeScript 2.4.1
+/*
+jasmine tests for Snowflake websocket
+*/
+describe('BuildUrl', function() {
+  it('should parse just protocol and host', function() {
+    return expect(WS.buildUrl('http', 'example.com')).toBe('http://example.com');
+  });
+  it('should handle different ports', function() {
+    expect(WS.buildUrl('http', 'example.com', 80)).toBe('http://example.com');
+    expect(WS.buildUrl('http', 'example.com', 81)).toBe('http://example.com:81');
+    expect(WS.buildUrl('http', 'example.com', 443)).toBe('http://example.com:443');
+    return expect(WS.buildUrl('http', 'example.com', 444)).toBe('http://example.com:444');
+  });
+  it('should handle paths', function() {
+    expect(WS.buildUrl('http', 'example.com', 80, '/')).toBe('http://example.com/');
+    expect(WS.buildUrl('http', 'example.com', 80, '/test?k=%#v')).toBe('http://example.com/test%3Fk%3D%25%23v');
+    return expect(WS.buildUrl('http', 'example.com', 80, '/test')).toBe('http://example.com/test');
+  });
+  it('should handle params', function() {
+    expect(WS.buildUrl('http', 'example.com', 80, '/test', [['k', '%#v']])).toBe('http://example.com/test?k=%25%23v');
+    return expect(WS.buildUrl('http', 'example.com', 80, '/test', [['a', 'b'], ['c', 'd']])).toBe('http://example.com/test?a=b&c=d');
+  });
+  it('should handle ips', function() {
+    expect(WS.buildUrl('http', '1.2.3.4')).toBe('http://1.2.3.4');
+    return expect(WS.buildUrl('http', '1:2::3:4')).toBe('http://[1:2::3:4]');
+  });
+  return it('should handle bogus', function() {
+    expect(WS.buildUrl('http', 'bog][us')).toBe('http://bog%5D%5Bus');
+    return expect(WS.buildUrl('http', 'bog:u]s')).toBe('http://bog%3Au%5Ds');
+  });
+});
diff --git a/proxy/ui.coffee b/proxy/ui.coffee
deleted file mode 100644
index fb3c0c2..0000000
--- a/proxy/ui.coffee
+++ /dev/null
@@ -1,125 +0,0 @@
-###
-All of Snowflake's DOM manipulation and inputs.
-###
-
-class UI
-  active: false
-  enabled: true
-
-  setStatus: (msg) ->
-
-  setActive: (connected) ->
-    @active = connected
-
-  log: (msg) ->
-
-
-class BadgeUI extends UI
-  $badge: null
-
-  constructor: ->
-    super()
-    @$badge = document.getElementById('badge')
-
-  setActive: (connected) ->
-    super connected
-    @$badge.className = if connected then 'active' else ''
-
-
-class DebugUI extends UI
-  # DOM elements references.
-  $msglog: null
-  $status: null
-
-  constructor: ->
-    super()
-    # Setup other DOM handlers if it's debug mode.
-    @$status = document.getElementById('status')
-    @$msglog = document.getElementById('msglog')
-    @$msglog.value = ''
-
-  # Status bar
-  setStatus: (msg) ->
-    txt = document.createTextNode('Status: ' + msg)
-    while @$status.firstChild
-      @$status.removeChild @$status.firstChild
-    @$status.appendChild txt
-
-  setActive: (connected) ->
-    super connected
-    @$msglog.className = if connected then 'active' else ''
-
-  log: (msg) ->
-    # Scroll to latest
-    @$msglog.value += msg + '\n'
-    @$msglog.scrollTop = @$msglog.scrollHeight
-
-
-class WebExtUI extends UI
-  port: null
-  stats: null
-
-  constructor: ->
-    super()
-    @initStats()
-    chrome.runtime.onConnect.addListener @onConnect
-
-  initStats: ->
-    @stats = [0]
-    setInterval (() =>
-      @stats.unshift 0
-      @stats.splice 24
-      @postActive()
-    ), 60 * 60 * 1000
-
-  initToggle: ->
-    getting = chrome.storage.local.get("snowflake-enabled", (result) =>
-      if result['snowflake-enabled'] != undefined
-        @enabled = result['snowflake-enabled']
-      else
-        log "Toggle state not yet saved"
-      @setEnabled @enabled
-    )
-
-  postActive: ->
-    @port?.postMessage
-      active: @active
-      total: @stats.reduce ((t, c) ->
-        t + c
-      ), 0
-      enabled: @enabled
-
-  onConnect: (port) =>
-    @port = port
-    port.onDisconnect.addListener @onDisconnect
-    port.onMessage.addListener @onMessage
-    @postActive()
-
-  onMessage: (m) =>
-    @enabled = m.enabled
-    @setEnabled @enabled
-    @postActive()
-    storing = chrome.storage.local.set({ "snowflake-enabled": @enabled },
-      () -> log "Stored toggle state")
-
-  onDisconnect: (port) =>
-    @port = null
-
-  setActive: (connected) ->
-    super connected
-    if connected then @stats[0] += 1
-    @postActive()
-    if @active
-      chrome.browserAction.setIcon
-        path:
-          32: "icons/status-running.png"
-    else
-      chrome.browserAction.setIcon
-        path:
-          32: "icons/status-on.png"
-
-  setEnabled: (enabled) ->
-    update()
-    chrome.browserAction.setIcon
-      path:
-        32: "icons/status-" + (if enabled then "on" else "off") + ".png"
diff --git a/proxy/ui.js b/proxy/ui.js
new file mode 100644
index 0000000..b3f0ae1
--- /dev/null
+++ b/proxy/ui.js
@@ -0,0 +1,197 @@
+// Generated by CoffeeScript 2.4.1
+  /*
+  All of Snowflake's DOM manipulation and inputs.
+  */
+var BadgeUI, DebugUI, UI, WebExtUI,
+  boundMethodCheck = function(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new Error('Bound instance method accessed before binding'); } };
+
+UI = (function() {
+  class UI {
+    setStatus(msg) {}
+
+    setActive(connected) {
+      return this.active = connected;
+    }
+
+    log(msg) {}
+
+  };
+
+  UI.prototype.active = false;
+
+  UI.prototype.enabled = true;
+
+  return UI;
+
+}).call(this);
+
+BadgeUI = (function() {
+  class BadgeUI extends UI {
+    constructor() {
+      super();
+      this.$badge = document.getElementById('badge');
+    }
+
+    setActive(connected) {
+      super.setActive(connected);
+      return this.$badge.className = connected ? 'active' : '';
+    }
+
+  };
+
+  BadgeUI.prototype.$badge = null;
+
+  return BadgeUI;
+
+}).call(this);
+
+DebugUI = (function() {
+  class DebugUI extends UI {
+    constructor() {
+      super();
+      // Setup other DOM handlers if it's debug mode.
+      this.$status = document.getElementById('status');
+      this.$msglog = document.getElementById('msglog');
+      this.$msglog.value = '';
+    }
+
+    // Status bar
+    setStatus(msg) {
+      var txt;
+      txt = document.createTextNode('Status: ' + msg);
+      while (this.$status.firstChild) {
+        this.$status.removeChild(this.$status.firstChild);
+      }
+      return this.$status.appendChild(txt);
+    }
+
+    setActive(connected) {
+      super.setActive(connected);
+      return this.$msglog.className = connected ? 'active' : '';
+    }
+
+    log(msg) {
+      // Scroll to latest
+      this.$msglog.value += msg + '\n';
+      return this.$msglog.scrollTop = this.$msglog.scrollHeight;
+    }
+
+  };
+
+  // DOM elements references.
+  DebugUI.prototype.$msglog = null;
+
+  DebugUI.prototype.$status = null;
+
+  return DebugUI;
+
+}).call(this);
+
+WebExtUI = (function() {
+  class WebExtUI extends UI {
+    constructor() {
+      super();
+      this.onConnect = this.onConnect.bind(this);
+      this.onMessage = this.onMessage.bind(this);
+      this.onDisconnect = this.onDisconnect.bind(this);
+      this.initStats();
+      chrome.runtime.onConnect.addListener(this.onConnect);
+    }
+
+    initStats() {
+      this.stats = [0];
+      return setInterval((() => {
+        this.stats.unshift(0);
+        this.stats.splice(24);
+        return this.postActive();
+      }), 60 * 60 * 1000);
+    }
+
+    initToggle() {
+      var getting;
+      return getting = chrome.storage.local.get("snowflake-enabled", (result) => {
+        if (result['snowflake-enabled'] !== void 0) {
+          this.enabled = result['snowflake-enabled'];
+        } else {
+          log("Toggle state not yet saved");
+        }
+        return this.setEnabled(this.enabled);
+      });
+    }
+
+    postActive() {
+      var ref;
+      return (ref = this.port) != null ? ref.postMessage({
+        active: this.active,
+        total: this.stats.reduce((function(t, c) {
+          return t + c;
+        }), 0),
+        enabled: this.enabled
+      }) : void 0;
+    }
+
+    onConnect(port) {
+      boundMethodCheck(this, WebExtUI);
+      this.port = port;
+      port.onDisconnect.addListener(this.onDisconnect);
+      port.onMessage.addListener(this.onMessage);
+      return this.postActive();
+    }
+
+    onMessage(m) {
+      var storing;
+      boundMethodCheck(this, WebExtUI);
+      this.enabled = m.enabled;
+      this.setEnabled(this.enabled);
+      this.postActive();
+      return storing = chrome.storage.local.set({
+        "snowflake-enabled": this.enabled
+      }, function() {
+        return log("Stored toggle state");
+      });
+    }
+
+    onDisconnect(port) {
+      boundMethodCheck(this, WebExtUI);
+      return this.port = null;
+    }
+
+    setActive(connected) {
+      super.setActive(connected);
+      if (connected) {
+        this.stats[0] += 1;
+      }
+      this.postActive();
+      if (this.active) {
+        return chrome.browserAction.setIcon({
+          path: {
+            32: "icons/status-running.png"
+          }
+        });
+      } else {
+        return chrome.browserAction.setIcon({
+          path: {
+            32: "icons/status-on.png"
+          }
+        });
+      }
+    }
+
+    setEnabled(enabled) {
+      update();
+      return chrome.browserAction.setIcon({
+        path: {
+          32: "icons/status-" + (enabled ? "on" : "off") + ".png"
+        }
+      });
+    }
+
+  };
+
+  WebExtUI.prototype.port = null;
+
+  WebExtUI.prototype.stats = null;
+
+  return WebExtUI;
+
+}).call(this);
diff --git a/proxy/util.coffee b/proxy/util.coffee
deleted file mode 100644
index 5f0461f..0000000
--- a/proxy/util.coffee
+++ /dev/null
@@ -1,204 +0,0 @@
-###
-A Coffeescript WebRTC snowflake proxy
-
-Contains helpers for parsing query strings and other utilities.
-###
-
-class Util
-  # It would not be effective for Tor Browser users to run the proxy.
-  # Do we seem to be running in Tor Browser? Check the user-agent string and for
-  # no listing of supported MIME types.
-  @TBB_UAS: [
-      'Mozilla/5.0 (Windows NT 6.1; rv:10.0) Gecko/20100101 Firefox/10.0'
-      'Mozilla/5.0 (Windows NT 6.1; rv:17.0) Gecko/20100101 Firefox/17.0'
-      'Mozilla/5.0 (Windows NT 6.1; rv:24.0) Gecko/20100101 Firefox/24.0'
-      'Mozilla/5.0 (Windows NT 6.1; rv:31.0) Gecko/20100101 Firefox/31.0'
-  ]
-  @mightBeTBB: ->
-    return Util.TBB_UAS.indexOf(window.navigator.userAgent) > -1 and
-            (window.navigator.mimeTypes and
-             window.navigator.mimeTypes.length == 0)
-
-  @genSnowflakeID: ->
-    Math.random().toString(36).substring(2)
-
-  @snowflakeIsDisabled = (cookieName) ->
-    cookies = Parse.cookie document.cookie
-    # Do nothing if snowflake has not been opted in by user.
-    if cookies[cookieName] != '1'
-      log 'Not opted-in. Please click the badge to change options.'
-      return true
-    # Also do nothing if running in Tor Browser.
-    if Util.mightBeTBB()
-      log 'Will not run within Tor Browser.'
-      return true
-    return false
-
-  @featureDetect = () ->
-    return typeof PeerConnection is 'function'
-
-class Query
-  ###
-  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
-  ###
-  @parse: (qs) ->
-    result = {}
-    strings = []
-    strings = qs.split '&' if qs
-    return result if 0 == strings.length
-    for string in strings
-      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, ' '))
-      result[name] = value if name not of result
-    result
-
-  # params is a list of (key, value) 2-tuples.
-  @buildString: (params) ->
-    parts = []
-    for param in params
-      parts.push encodeURIComponent(param[0]) + '=' +
-                 encodeURIComponent(param[1])
-    parts.join '&'
-
-
-class Parse
-  # Parse a cookie data string (usually document.cookie). The return type is an
-  # object mapping cookies names to values. Returns null on error.
-  # http://www.w3.org/TR/DOM-Level-2-HTML/html.html#ID-8747038
-  @cookie: (cookies) ->
-    result = {}
-    strings = []
-    strings = cookies.split ';' if cookies
-    for string in strings
-      j = string.indexOf '='
-      return null if -1 == j
-      name  = decodeURIComponent string.substr(0, j).trim()
-      value = decodeURIComponent string.substr(j + 1).trim()
-      result[name] = value if name not of result
-    result
-
-  # Parse an address in the form 'host:port'. Returns an Object with keys 'host'
-  # (String) and 'port' (int). Returns null on error.
-  @address: (spec) ->
-    m = null
-    # IPv6 syntax.
-    m = spec.match(/^\[([\0-9a-fA-F:.]+)\]:([0-9]+)$/) if !m
-    # IPv4 syntax.
-    m = spec.match(/^([0-9.]+):([0-9]+)$/) if !m
-    # TODO: Domain match
-    return null if !m
-
-    host = m[1]
-    port = parseInt(m[2], 10)
-    if isNaN(port) || port < 0 || port > 65535
-      return null
-    { host: host, port: port }
-
-  # Parse a count of bytes. A suffix of 'k', 'm', or 'g' (or uppercase)
-  # does what you would think. Returns null on error.
-  @byteCount: (spec) ->
-    UNITS = {
-      k: 1024, m: 1024 * 1024, g: 1024 * 1024 * 1024
-      K: 1024, M: 1024 * 1024, G: 1024 * 1024 * 1024
-    }
-    matches = spec.match /^(\d+(?:\.\d*)?)(\w*)$/
-    return null if null == matches
-    count = Number matches[1]
-    return null if isNaN count
-    if '' == matches[2]
-      units = 1
-    else
-      units = UNITS[matches[2]]
-      return null if null == units
-    count * Number(units)
-
-  # Parse a connection-address out of the "c=" Connection Data field of a
-  # session description. Return undefined if none is found.
-  # https://tools.ietf.org/html/rfc4566#section-5.7
-  @ipFromSDP: (sdp) ->
-    for pattern in [
-      /^c=IN IP4 ([\d.]+)(?:(?:\/\d+)?\/\d+)?(:? |$)/m,
-      /^c=IN IP6 ([0-9A-Fa-f:.]+)(?:\/\d+)?(:? |$)/m,
-    ]
-      m = pattern.exec(sdp)
-      return m[1] if m?
-
-
-class Params
-  @getBool: (query, param, defaultValue) ->
-    val = query[param]
-    return defaultValue if undefined == val
-    return true if 'true' == val || '1' == val || '' == val
-    return false if 'false' == val || '0' == val
-    return null
-
-  # Get an object value and parse it as a byte count. Example byte counts are
-  # '100' and '1.3m'. Returns |defaultValue| if param is not a key. Return null
-  # on a parsing error.
-  @getByteCount: (query, param, defaultValue) ->
-    spec = query[param]
-    return defaultValue if undefined == spec
-    Parse.byteCount spec
-
-  # Get an object value and parse it as an address spec. Returns |defaultValue|
-  # if param is not a key. Returns null on a parsing error.
-  @getAddress: (query, param, defaultValue) ->
-    val = query[param]
-    return defaultValue if undefined == val
-    Parse.address val
-
-  # Get an object value and return it as a string. Returns default_val if param
-  # is not a key.
-  @getString: (query, param, defaultValue) ->
-    val = query[param]
-    return defaultValue if undefined == val
-    val
-
-class BucketRateLimit
-  amount: 0.0
-  lastUpdate: new Date()
-
-  constructor: (@capacity, @time) ->
-
-  age: ->
-    now = new Date()
-    delta = (now - @lastUpdate) / 1000.0
-    @lastUpdate = now
-    @amount -= delta * @capacity / @time
-    @amount = 0.0 if @amount < 0.0
-
-  update: (n) ->
-    @age()
-    @amount += n
-    @amount <= @capacity
-
-  # How many seconds in the future will the limit expire?
-  when: ->
-    @age()
-    (@amount - @capacity) / (@capacity / @time)
-
-  isLimited: ->
-    @age()
-    @amount > @capacity
-
-
-# A rate limiter that never limits.
-class DummyRateLimit
-  constructor: (@capacity, @time) ->
-  update: (n) -> true
-  when: -> 0.0
-  isLimited: -> false
diff --git a/proxy/util.js b/proxy/util.js
new file mode 100644
index 0000000..1feeb5d
--- /dev/null
+++ b/proxy/util.js
@@ -0,0 +1,321 @@
+// Generated by CoffeeScript 2.4.1
+/*
+A Coffeescript WebRTC snowflake proxy
+
+Contains helpers for parsing query strings and other utilities.
+*/
+var BucketRateLimit, DummyRateLimit, Params, Parse, Query, Util;
+
+Util = (function() {
+  class Util {
+    static mightBeTBB() {
+      return Util.TBB_UAS.indexOf(window.navigator.userAgent) > -1 && (window.navigator.mimeTypes && window.navigator.mimeTypes.length === 0);
+    }
+
+    static genSnowflakeID() {
+      return Math.random().toString(36).substring(2);
+    }
+
+    static snowflakeIsDisabled(cookieName) {
+      var cookies;
+      cookies = Parse.cookie(document.cookie);
+      // Do nothing if snowflake has not been opted in by user.
+      if (cookies[cookieName] !== '1') {
+        log('Not opted-in. Please click the badge to change options.');
+        return true;
+      }
+      // Also do nothing if running in Tor Browser.
+      if (Util.mightBeTBB()) {
+        log('Will not run within Tor Browser.');
+        return true;
+      }
+      return false;
+    }
+
+    static featureDetect() {
+      return typeof PeerConnection === 'function';
+    }
+
+  };
+
+  // It would not be effective for Tor Browser users to run the proxy.
+  // Do we seem to be running in Tor Browser? Check the user-agent string and for
+  // no listing of supported MIME types.
+  Util.TBB_UAS = ['Mozilla/5.0 (Windows NT 6.1; rv:10.0) Gecko/20100101 Firefox/10.0', 'Mozilla/5.0 (Windows NT 6.1; rv:17.0) Gecko/20100101 Firefox/17.0', 'Mozilla/5.0 (Windows NT 6.1; rv:24.0) Gecko/20100101 Firefox/24.0', 'Mozilla/5.0 (Windows NT 6.1; rv:31.0) Gecko/20100101 Firefox/31.0'];
+
+  return Util;
+
+}).call(this);
+
+Query = class Query {
+  /*
+  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
+  */
+  static parse(qs) {
+    var i, j, len, name, result, string, strings, value;
+    result = {};
+    strings = [];
+    if (qs) {
+      strings = qs.split('&');
+    }
+    if (0 === strings.length) {
+      return result;
+    }
+    for (i = 0, len = strings.length; i < len; i++) {
+      string = strings[i];
+      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;
+  }
+
+  // params is a list of (key, value) 2-tuples.
+  static buildString(params) {
+    var i, len, param, parts;
+    parts = [];
+    for (i = 0, len = params.length; i < len; i++) {
+      param = params[i];
+      parts.push(encodeURIComponent(param[0]) + '=' + encodeURIComponent(param[1]));
+    }
+    return parts.join('&');
+  }
+
+};
+
+Parse = class Parse {
+  // Parse a cookie data string (usually document.cookie). The return type is an
+  // object mapping cookies names to values. Returns null on error.
+  // http://www.w3.org/TR/DOM-Level-2-HTML/html.html#ID-8747038
+  static cookie(cookies) {
+    var i, j, len, name, result, string, strings, value;
+    result = {};
+    strings = [];
+    if (cookies) {
+      strings = cookies.split(';');
+    }
+    for (i = 0, len = strings.length; i < len; i++) {
+      string = strings[i];
+      j = string.indexOf('=');
+      if (-1 === j) {
+        return null;
+      }
+      name = decodeURIComponent(string.substr(0, j).trim());
+      value = decodeURIComponent(string.substr(j + 1).trim());
+      if (!(name in result)) {
+        result[name] = value;
+      }
+    }
+    return result;
+  }
+
+  // Parse an address in the form 'host:port'. Returns an Object with keys 'host'
+  // (String) and 'port' (int). Returns null on error.
+  static address(spec) {
+    var host, m, port;
+    m = null;
+    if (!m) {
+      // IPv6 syntax.
+      m = spec.match(/^\[([\0-9a-fA-F:.]+)\]:([0-9]+)$/);
+    }
+    if (!m) {
+      // IPv4 syntax.
+      m = spec.match(/^([0-9.]+):([0-9]+)$/);
+    }
+    if (!m) {
+      // TODO: Domain match
+      return null;
+    }
+    host = m[1];
+    port = parseInt(m[2], 10);
+    if (isNaN(port) || port < 0 || port > 65535) {
+      return null;
+    }
+    return {
+      host: host,
+      port: port
+    };
+  }
+
+  // Parse a count of bytes. A suffix of 'k', 'm', or 'g' (or uppercase)
+  // does what you would think. Returns null on error.
+  static byteCount(spec) {
+    var UNITS, count, matches, units;
+    UNITS = {
+      k: 1024,
+      m: 1024 * 1024,
+      g: 1024 * 1024 * 1024,
+      K: 1024,
+      M: 1024 * 1024,
+      G: 1024 * 1024 * 1024
+    };
+    matches = spec.match(/^(\d+(?:\.\d*)?)(\w*)$/);
+    if (null === matches) {
+      return null;
+    }
+    count = Number(matches[1]);
+    if (isNaN(count)) {
+      return null;
+    }
+    if ('' === matches[2]) {
+      units = 1;
+    } else {
+      units = UNITS[matches[2]];
+      if (null === units) {
+        return null;
+      }
+    }
+    return count * Number(units);
+  }
+
+  // Parse a connection-address out of the "c=" Connection Data field of a
+  // session description. Return undefined if none is found.
+  // https://tools.ietf.org/html/rfc4566#section-5.7
+  static ipFromSDP(sdp) {
+    var i, len, m, pattern, ref;
+    ref = [/^c=IN IP4 ([\d.]+)(?:(?:\/\d+)?\/\d+)?(:? |$)/m, /^c=IN IP6 ([0-9A-Fa-f:.]+)(?:\/\d+)?(:? |$)/m];
+    for (i = 0, len = ref.length; i < len; i++) {
+      pattern = ref[i];
+      m = pattern.exec(sdp);
+      if (m != null) {
+        return m[1];
+      }
+    }
+  }
+
+};
+
+Params = class Params {
+  static getBool(query, param, defaultValue) {
+    var val;
+    val = query[param];
+    if (void 0 === val) {
+      return defaultValue;
+    }
+    if ('true' === val || '1' === val || '' === val) {
+      return true;
+    }
+    if ('false' === val || '0' === val) {
+      return false;
+    }
+    return null;
+  }
+
+  // Get an object value and parse it as a byte count. Example byte counts are
+  // '100' and '1.3m'. Returns |defaultValue| if param is not a key. Return null
+  // on a parsing error.
+  static getByteCount(query, param, defaultValue) {
+    var spec;
+    spec = query[param];
+    if (void 0 === spec) {
+      return defaultValue;
+    }
+    return Parse.byteCount(spec);
+  }
+
+  // Get an object value and parse it as an address spec. Returns |defaultValue|
+  // if param is not a key. Returns null on a parsing error.
+  static getAddress(query, param, defaultValue) {
+    var val;
+    val = query[param];
+    if (void 0 === val) {
+      return defaultValue;
+    }
+    return Parse.address(val);
+  }
+
+  // Get an object value and return it as a string. Returns default_val if param
+  // is not a key.
+  static getString(query, param, defaultValue) {
+    var val;
+    val = query[param];
+    if (void 0 === val) {
+      return defaultValue;
+    }
+    return val;
+  }
+
+};
+
+BucketRateLimit = (function() {
+  class BucketRateLimit {
+    constructor(capacity, time) {
+      this.capacity = capacity;
+      this.time = time;
+    }
+
+    age() {
+      var delta, now;
+      now = new Date();
+      delta = (now - this.lastUpdate) / 1000.0;
+      this.lastUpdate = now;
+      this.amount -= delta * this.capacity / this.time;
+      if (this.amount < 0.0) {
+        return this.amount = 0.0;
+      }
+    }
+
+    update(n) {
+      this.age();
+      this.amount += n;
+      return this.amount <= this.capacity;
+    }
+
+    // How many seconds in the future will the limit expire?
+    when() {
+      this.age();
+      return (this.amount - this.capacity) / (this.capacity / this.time);
+    }
+
+    isLimited() {
+      this.age();
+      return this.amount > this.capacity;
+    }
+
+  };
+
+  BucketRateLimit.prototype.amount = 0.0;
+
+  BucketRateLimit.prototype.lastUpdate = new Date();
+
+  return BucketRateLimit;
+
+}).call(this);
+
+// A rate limiter that never limits.
+DummyRateLimit = class DummyRateLimit {
+  constructor(capacity, time) {
+    this.capacity = capacity;
+    this.time = time;
+  }
+
+  update(n) {
+    return true;
+  }
+
+  when() {
+    return 0.0;
+  }
+
+  isLimited() {
+    return false;
+  }
+
+};
diff --git a/proxy/websocket.coffee b/proxy/websocket.coffee
deleted file mode 100644
index 1e75ac1..0000000
--- a/proxy/websocket.coffee
+++ /dev/null
@@ -1,61 +0,0 @@
-###
-Only websocket-specific stuff.
-###
-
-class WS
-  @WSS_ENABLED: true
-  @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.
-  @buildUrl: (scheme, host, port, path, params) ->
-    parts = []
-    parts.push(encodeURIComponent scheme)
-    parts.push '://'
-
-    # If it contains a colon but no square brackets, treat it as IPv6.
-    if host.match(/:/) && !host.match(/[[\]]/)
-      parts.push '['
-      parts.push host
-      parts.push ']'
-    else
-      parts.push(encodeURIComponent host)
-
-    if undefined != port && @DEFAULT_PORTS[scheme] != port
-      parts.push ':'
-      parts.push(encodeURIComponent port.toString())
-
-    if undefined != path && '' != 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 /[^\/]+/, (m) ->
-        encodeURIComponent m
-      parts.push path
-
-    if undefined != params
-      parts.push '?'
-      parts.push Query.buildString params
-
-    parts.join ''
-
-  @makeWebsocket: (addr, params) ->
-    wsProtocol = if @WSS_ENABLED then 'wss' else 'ws'
-    url = @buildUrl wsProtocol, addr.host, addr.port, '/', params
-    ws = new WebSocket url
-    ###
-    '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'
-    ws
diff --git a/proxy/websocket.js b/proxy/websocket.js
new file mode 100644
index 0000000..9d4ec60
--- /dev/null
+++ b/proxy/websocket.js
@@ -0,0 +1,70 @@
+// Generated by CoffeeScript 2.4.1
+/*
+Only websocket-specific stuff.
+*/
+var WS;
+
+WS = (function() {
+  class WS {
+    // Build an escaped URL string from unescaped components. Only scheme and host
+    // are required. See RFC 3986, section 3.
+    static buildUrl(scheme, host, port, path, params) {
+      var parts;
+      parts = [];
+      parts.push(encodeURIComponent(scheme));
+      parts.push('://');
+      // If it contains a colon but no square brackets, treat it as IPv6.
+      if (host.match(/:/) && !host.match(/[[\]]/)) {
+        parts.push('[');
+        parts.push(host);
+        parts.push(']');
+      } else {
+        parts.push(encodeURIComponent(host));
+      }
+      if (void 0 !== port && this.DEFAULT_PORTS[scheme] !== port) {
+        parts.push(':');
+        parts.push(encodeURIComponent(port.toString()));
+      }
+      if (void 0 !== path && '' !== path) {
+        if (!path.match(/^\//)) {
+          path = '/' + path;
+        }
+        path = path.replace(/[^\/]+/, function(m) {
+          return encodeURIComponent(m);
+        });
+        parts.push(path);
+      }
+      if (void 0 !== params) {
+        parts.push('?');
+        parts.push(Query.buildString(params));
+      }
+      return parts.join('');
+    }
+
+    static makeWebsocket(addr, params) {
+      var url, ws, wsProtocol;
+      wsProtocol = this.WSS_ENABLED ? 'wss' : 'ws';
+      url = this.buildUrl(wsProtocol, addr.host, addr.port, '/', params);
+      ws = new WebSocket(url);
+      /*
+      '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;
+    }
+
+  };
+
+  WS.WSS_ENABLED = true;
+
+  WS.DEFAULT_PORTS = {
+    http: 80,
+    https: 443
+  };
+
+  return WS;
+
+}).call(this);





More information about the tor-commits mailing list