[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [snowflake/master] Compile coffee files and remove them
commit 31ad9566e64ca1236242966b7de6d045e25d837a
Author: Arlo Breault <arlolra@xxxxxxxxx>
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@xxxxxxxxxxx (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@xxxxxxxxxxx (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@xxxxxxxxxxx (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@xxxxxxxxxxx (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);
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits