commit 31ad9566e64ca1236242966b7de6d045e25d837a Author: Arlo Breault arlolra@gmail.com Date: Sat Jul 6 15:13:06 2019 +0200
Compile coffee files and remove them
With,
./node_modules/.bin/coffee -b -c Cakefile `find . -path ./node_modules -prune -o -name '*.coffee'` --- proxy/Cakefile | 83 ---------- proxy/Cakefile.js | 84 ++++++++++ proxy/broker.coffee | 90 ----------- proxy/broker.js | 126 +++++++++++++++ proxy/config.coffee | 25 --- proxy/config.js | 43 ++++++ proxy/init-badge.coffee | 64 -------- proxy/init-badge.js | 75 +++++++++ proxy/init-node.coffee | 19 --- proxy/init-node.js | 27 ++++ proxy/init-webext.coffee | 58 ------- proxy/init-webext.js | 76 +++++++++ proxy/proxypair.coffee | 185 ---------------------- proxy/proxypair.js | 262 ++++++++++++++++++++++++++++++++ proxy/shims.coffee | 35 ----- proxy/shims.js | 34 +++++ proxy/snowflake.coffee | 132 ---------------- proxy/snowflake.js | 182 ++++++++++++++++++++++ proxy/spec/broker.spec.coffee | 92 ----------- proxy/spec/broker.spec.js | 119 +++++++++++++++ proxy/spec/init.spec.coffee | 28 ---- proxy/spec/init.spec.js | 34 +++++ proxy/spec/proxypair.spec.coffee | 125 --------------- proxy/spec/proxypair.spec.js | 143 +++++++++++++++++ proxy/spec/snowflake.spec.coffee | 67 -------- proxy/spec/snowflake.spec.js | 114 ++++++++++++++ proxy/spec/ui.spec.coffee | 57 ------- proxy/spec/ui.spec.js | 84 ++++++++++ proxy/spec/util.spec.coffee | 236 ---------------------------- proxy/spec/util.spec.js | 254 +++++++++++++++++++++++++++++++ proxy/spec/websocket.spec.coffee | 39 ----- proxy/spec/websocket.spec.js | 32 ++++ proxy/ui.coffee | 125 --------------- proxy/ui.js | 197 ++++++++++++++++++++++++ proxy/util.coffee | 204 ------------------------- proxy/util.js | 321 +++++++++++++++++++++++++++++++++++++++ proxy/websocket.coffee | 61 -------- proxy/websocket.js | 70 +++++++++ 38 files changed, 2277 insertions(+), 1725 deletions(-)
diff --git a/proxy/Cakefile b/proxy/Cakefile deleted file mode 100644 index ba2e5ec..0000000 --- a/proxy/Cakefile +++ /dev/null @@ -1,83 +0,0 @@ -fs = require 'fs' -{ exec, spawn, execSync } = require 'child_process' - -# All coffeescript files required. -FILES = [ - 'broker.coffee' - 'config.coffee' - 'proxypair.coffee' - 'snowflake.coffee' - 'ui.coffee' - 'util.coffee' - 'websocket.coffee' - - 'shims.coffee' -] - -INITS = [ - 'init-badge.coffee' - 'init-node.coffee' - 'init-webext.coffee' -] - -FILES_SPEC = [ - 'spec/broker.spec.coffee' - 'spec/init.spec.coffee' - 'spec/proxypair.spec.coffee' - 'spec/snowflake.spec.coffee' - 'spec/ui.spec.coffee' - 'spec/util.spec.coffee' - 'spec/websocket.spec.coffee' -] - -OUTFILE = 'snowflake.js' -STATIC = 'static' - -copyStaticFiles = -> - exec 'cp ' + STATIC + '/* build/' - -compileCoffee = (outDir, init) -> - files = FILES.concat('init-' + init + '.coffee') - exec 'cat ' + files.join(' ') + ' | coffee -cs > ' + outDir + '/' + OUTFILE, (err, stdout, stderr) -> - throw err if err - -task 'test', 'snowflake unit tests', -> - exec 'mkdir -p test' - exec 'jasmine init >&-' - # Simply concat all the files because we're not using node exports. - jasmineFiles = FILES.concat('init-badge.coffee', FILES_SPEC) - outFile = 'test/bundle.spec.coffee' - exec 'echo "TESTING = true" > ' + outFile - exec 'cat ' + jasmineFiles.join(' ') + ' | cat >> ' + outFile - execSync 'coffee -cb ' + outFile - proc = spawn 'jasmine', ['test/bundle.spec.js'], { - stdio: 'inherit' - } - proc.on "exit", (code) -> process.exit code - -task 'build', 'build the snowflake proxy', -> - exec 'mkdir -p build' - copyStaticFiles() - compileCoffee('build', 'badge') - console.log 'Snowflake prepared.' - -task 'webext', 'build the webextension', -> - exec 'mkdir -p webext' - compileCoffee('webext', 'webext') - console.log 'Webextension prepared.' - -task 'node', 'build the node binary', -> - exec 'mkdir -p build' - compileCoffee('build', 'node') - console.log 'Node prepared.' - -task 'lint', 'ensure idiomatic coffeescript', -> - filesAll = FILES.concat(INITS, FILES_SPEC) - proc = spawn 'coffeelint', filesAll, { - file: 'coffeelint.json' - stdio: 'inherit' - } - proc.on "exit", (code) -> process.exit code - -task 'clean', 'remove all built files', -> - exec 'rm -r build' diff --git a/proxy/Cakefile.js b/proxy/Cakefile.js new file mode 100644 index 0000000..c7bc625 --- /dev/null +++ b/proxy/Cakefile.js @@ -0,0 +1,84 @@ +// Generated by CoffeeScript 2.4.1 +var FILES, FILES_SPEC, INITS, OUTFILE, STATIC, compileCoffee, copyStaticFiles, exec, execSync, fs, spawn; + +fs = require('fs'); + +({exec, spawn, execSync} = require('child_process')); + +// All coffeescript files required. +FILES = ['broker.coffee', 'config.coffee', 'proxypair.coffee', 'snowflake.coffee', 'ui.coffee', 'util.coffee', 'websocket.coffee', 'shims.coffee']; + +INITS = ['init-badge.coffee', 'init-node.coffee', 'init-webext.coffee']; + +FILES_SPEC = ['spec/broker.spec.coffee', 'spec/init.spec.coffee', 'spec/proxypair.spec.coffee', 'spec/snowflake.spec.coffee', 'spec/ui.spec.coffee', 'spec/util.spec.coffee', 'spec/websocket.spec.coffee']; + +OUTFILE = 'snowflake.js'; + +STATIC = 'static'; + +copyStaticFiles = function() { + return exec('cp ' + STATIC + '/* build/'); +}; + +compileCoffee = function(outDir, init) { + var files; + files = FILES.concat('init-' + init + '.coffee'); + return exec('cat ' + files.join(' ') + ' | coffee -cs > ' + outDir + '/' + OUTFILE, function(err, stdout, stderr) { + if (err) { + throw err; + } + }); +}; + +task('test', 'snowflake unit tests', function() { + var jasmineFiles, outFile, proc; + exec('mkdir -p test'); + exec('jasmine init >&-'); + // Simply concat all the files because we're not using node exports. + jasmineFiles = FILES.concat('init-badge.coffee', FILES_SPEC); + outFile = 'test/bundle.spec.coffee'; + exec('echo "TESTING = true" > ' + outFile); + exec('cat ' + jasmineFiles.join(' ') + ' | cat >> ' + outFile); + execSync('coffee -cb ' + outFile); + proc = spawn('jasmine', ['test/bundle.spec.js'], { + stdio: 'inherit' + }); + return proc.on("exit", function(code) { + return process.exit(code); + }); +}); + +task('build', 'build the snowflake proxy', function() { + exec('mkdir -p build'); + copyStaticFiles(); + compileCoffee('build', 'badge'); + return console.log('Snowflake prepared.'); +}); + +task('webext', 'build the webextension', function() { + exec('mkdir -p webext'); + compileCoffee('webext', 'webext'); + return console.log('Webextension prepared.'); +}); + +task('node', 'build the node binary', function() { + exec('mkdir -p build'); + compileCoffee('build', 'node'); + return console.log('Node prepared.'); +}); + +task('lint', 'ensure idiomatic coffeescript', function() { + var filesAll, proc; + filesAll = FILES.concat(INITS, FILES_SPEC); + proc = spawn('coffeelint', filesAll, { + file: 'coffeelint.json', + stdio: 'inherit' + }); + return proc.on("exit", function(code) { + return process.exit(code); + }); +}); + +task('clean', 'remove all built files', function() { + return exec('rm -r build'); +}); diff --git a/proxy/broker.coffee b/proxy/broker.coffee deleted file mode 100644 index ca8a2e5..0000000 --- a/proxy/broker.coffee +++ /dev/null @@ -1,90 +0,0 @@ -### -Communication with the snowflake broker. - -Browser snowflakes must register with the broker in order -to get assigned to clients. -### - -# Represents a broker running remotely. -class Broker - @STATUS: - OK: 200 - GONE: 410 - GATEWAY_TIMEOUT: 504 - - @MESSAGE: - TIMEOUT: 'Timed out waiting for a client offer.' - UNEXPECTED: 'Unexpected status.' - - clients: 0 - - # When interacting with the Broker, snowflake must generate a unique session - # ID so the Broker can keep track of each proxy's signalling channels. - # On construction, this Broker object does not do anything until - # |getClientOffer| is called. - constructor: (@url) -> - @clients = 0 - # Ensure url has the right protocol + trailing slash. - @url = 'http://' + @url if 0 == @url.indexOf('localhost', 0) - @url = 'https://' + @url if 0 != @url.indexOf('http', 0) - @url += '/' if '/' != @url.substr -1 - - # Promises some client SDP Offer. - # Registers this Snowflake with the broker using an HTTP POST request, and - # waits for a response containing some client offer that the Broker chooses - # for this proxy.. - # TODO: Actually support multiple clients. - getClientOffer: (id) => - new Promise (fulfill, reject) => - xhr = new XMLHttpRequest() - xhr.onreadystatechange = -> - return if xhr.DONE != xhr.readyState - switch xhr.status - when Broker.STATUS.OK - fulfill xhr.responseText # Should contain offer. - when Broker.STATUS.GATEWAY_TIMEOUT - reject Broker.MESSAGE.TIMEOUT - else - log 'Broker ERROR: Unexpected ' + xhr.status + - ' - ' + xhr.statusText - snowflake.ui.setStatus ' failure. Please refresh.' - reject Broker.MESSAGE.UNEXPECTED - @_xhr = xhr # Used by spec to fake async Broker interaction - @_postRequest id, xhr, 'proxy', id - - # Assumes getClientOffer happened, and a WebRTC SDP answer has been generated. - # Sends it back to the broker, which passes it to back to the original client. - sendAnswer: (id, answer) -> - dbg id + ' - Sending answer back to broker...\n' - dbg answer.sdp - xhr = new XMLHttpRequest() - xhr.onreadystatechange = -> - return if xhr.DONE != xhr.readyState - switch xhr.status - when Broker.STATUS.OK - dbg 'Broker: Successfully replied with answer.' - dbg xhr.responseText - when Broker.STATUS.GONE - dbg 'Broker: No longer valid to reply with answer.' - else - dbg 'Broker ERROR: Unexpected ' + xhr.status + - ' - ' + xhr.statusText - snowflake.ui.setStatus ' failure. Please refresh.' - @_postRequest id, xhr, 'answer', JSON.stringify(answer) - - # urlSuffix for the broker is different depending on what action - # is desired. - _postRequest: (id, xhr, urlSuffix, payload) => - try - xhr.open 'POST', @url + urlSuffix - xhr.setRequestHeader('X-Session-ID', id) - catch err - ### - An exception happens here when, for example, NoScript allows the domain - on which the proxy badge runs, but not the domain to which it's trying - to make the HTTP xhr. The exception message is like "Component - returned failure code: 0x805e0006 [nsIXMLHttpRequest.open]" on Firefox. - ### - log 'Broker: exception while connecting: ' + err.message - return - xhr.send payload diff --git a/proxy/broker.js b/proxy/broker.js new file mode 100644 index 0000000..f7af5bb --- /dev/null +++ b/proxy/broker.js @@ -0,0 +1,126 @@ +// Generated by CoffeeScript 2.4.1 +/* +Communication with the snowflake broker. + +Browser snowflakes must register with the broker in order +to get assigned to clients. +*/ +var Broker; + +Broker = (function() { + // Represents a broker running remotely. + class Broker { + // When interacting with the Broker, snowflake must generate a unique session + // ID so the Broker can keep track of each proxy's signalling channels. + // On construction, this Broker object does not do anything until + // |getClientOffer| is called. + constructor(url) { + // Promises some client SDP Offer. + // Registers this Snowflake with the broker using an HTTP POST request, and + // waits for a response containing some client offer that the Broker chooses + // for this proxy.. + // TODO: Actually support multiple clients. + this.getClientOffer = this.getClientOffer.bind(this); + // urlSuffix for the broker is different depending on what action + // is desired. + this._postRequest = this._postRequest.bind(this); + this.url = url; + this.clients = 0; + if (0 === this.url.indexOf('localhost', 0)) { + // Ensure url has the right protocol + trailing slash. + this.url = 'http://' + this.url; + } + if (0 !== this.url.indexOf('http', 0)) { + this.url = 'https://' + this.url; + } + if ('/' !== this.url.substr(-1)) { + this.url += '/'; + } + } + + getClientOffer(id) { + return new Promise((fulfill, reject) => { + var xhr; + xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function() { + if (xhr.DONE !== xhr.readyState) { + return; + } + switch (xhr.status) { + case Broker.STATUS.OK: + return fulfill(xhr.responseText); // Should contain offer. + case Broker.STATUS.GATEWAY_TIMEOUT: + return reject(Broker.MESSAGE.TIMEOUT); + default: + log('Broker ERROR: Unexpected ' + xhr.status + ' - ' + xhr.statusText); + snowflake.ui.setStatus(' failure. Please refresh.'); + return reject(Broker.MESSAGE.UNEXPECTED); + } + }; + this._xhr = xhr; // Used by spec to fake async Broker interaction + return this._postRequest(id, xhr, 'proxy', id); + }); + } + + // Assumes getClientOffer happened, and a WebRTC SDP answer has been generated. + // Sends it back to the broker, which passes it to back to the original client. + sendAnswer(id, answer) { + var xhr; + dbg(id + ' - Sending answer back to broker...\n'); + dbg(answer.sdp); + xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function() { + if (xhr.DONE !== xhr.readyState) { + return; + } + switch (xhr.status) { + case Broker.STATUS.OK: + dbg('Broker: Successfully replied with answer.'); + return dbg(xhr.responseText); + case Broker.STATUS.GONE: + return dbg('Broker: No longer valid to reply with answer.'); + default: + dbg('Broker ERROR: Unexpected ' + xhr.status + ' - ' + xhr.statusText); + return snowflake.ui.setStatus(' failure. Please refresh.'); + } + }; + return this._postRequest(id, xhr, 'answer', JSON.stringify(answer)); + } + + _postRequest(id, xhr, urlSuffix, payload) { + var err; + try { + xhr.open('POST', this.url + urlSuffix); + xhr.setRequestHeader('X-Session-ID', id); + } catch (error) { + err = error; + /* + An exception happens here when, for example, NoScript allows the domain + on which the proxy badge runs, but not the domain to which it's trying + to make the HTTP xhr. The exception message is like "Component + returned failure code: 0x805e0006 [nsIXMLHttpRequest.open]" on Firefox. + */ + log('Broker: exception while connecting: ' + err.message); + return; + } + return xhr.send(payload); + } + + }; + + Broker.STATUS = { + OK: 200, + GONE: 410, + GATEWAY_TIMEOUT: 504 + }; + + Broker.MESSAGE = { + TIMEOUT: 'Timed out waiting for a client offer.', + UNEXPECTED: 'Unexpected status.' + }; + + Broker.prototype.clients = 0; + + return Broker; + +}).call(this); diff --git a/proxy/config.coffee b/proxy/config.coffee deleted file mode 100644 index 43dca56..0000000 --- a/proxy/config.coffee +++ /dev/null @@ -1,25 +0,0 @@ -class Config - brokerUrl: 'snowflake-broker.bamsoftware.com' - relayAddr: - host: 'snowflake.bamsoftware.com' - port: '443' - # Original non-wss relay: - # host: '192.81.135.242' - # port: 9902 - - cookieName: "snowflake-allow" - - # Bytes per second. Set to undefined to disable limit. - rateLimitBytes: undefined - minRateLimit: 10 * 1024 - rateLimitHistory: 5.0 - defaultBrokerPollInterval: 5.0 * 1000 - - maxNumClients: 1 - connectionsPerClient: 1 - - # TODO: Different ICE servers. - pcConfig: - iceServers: [ - { urls: ['stun:stun.l.google.com:19302'] } - ] diff --git a/proxy/config.js b/proxy/config.js new file mode 100644 index 0000000..69d7b75 --- /dev/null +++ b/proxy/config.js @@ -0,0 +1,43 @@ +// Generated by CoffeeScript 2.4.1 +var Config; + +Config = (function() { + class Config {}; + + Config.prototype.brokerUrl = 'snowflake-broker.bamsoftware.com'; + + Config.prototype.relayAddr = { + host: 'snowflake.bamsoftware.com', + port: '443' + }; + + // Original non-wss relay: + // host: '192.81.135.242' + // port: 9902 + Config.prototype.cookieName = "snowflake-allow"; + + // Bytes per second. Set to undefined to disable limit. + Config.prototype.rateLimitBytes = void 0; + + Config.prototype.minRateLimit = 10 * 1024; + + Config.prototype.rateLimitHistory = 5.0; + + Config.prototype.defaultBrokerPollInterval = 5.0 * 1000; + + Config.prototype.maxNumClients = 1; + + Config.prototype.connectionsPerClient = 1; + + // TODO: Different ICE servers. + Config.prototype.pcConfig = { + iceServers: [ + { + urls: ['stun:stun.l.google.com:19302'] + } + ] + }; + + return Config; + +}).call(this); diff --git a/proxy/init-badge.coffee b/proxy/init-badge.coffee deleted file mode 100644 index c4c9604..0000000 --- a/proxy/init-badge.coffee +++ /dev/null @@ -1,64 +0,0 @@ -### -Entry point. -### - -if (not TESTING? or not TESTING) and not Util.featureDetect() - console.log 'webrtc feature not detected. shutting down' - return - -snowflake = null - -query = Query.parse(location) -debug = Params.getBool(query, 'debug', false) -silenceNotifications = Params.getBool(query, 'silent', false) - -# Log to both console and UI if applicable. -# Requires that the snowflake and UI objects are hooked up in order to -# log to console. -log = (msg) -> - console.log 'Snowflake: ' + msg - snowflake?.ui.log msg - -dbg = (msg) -> log msg if debug or (snowflake?.ui instanceof DebugUI) - -init = () -> - config = new Config - - if 'off' != query['ratelimit'] - config.rateLimitBytes = Params.getByteCount( - query,'ratelimit', config.rateLimitBytes - ) - - ui = null - if (document.getElementById('badge') != null) - ui = new BadgeUI() - else if (document.getElementById('status') != null) - ui = new DebugUI() - else - ui = new UI() - - broker = new Broker config.brokerUrl - snowflake = new Snowflake config, ui, broker - - log '== snowflake proxy ==' - if Util.snowflakeIsDisabled(config.cookieName) - # Do not activate the proxy if any number of conditions are true. - log 'Currently not active.' - return - - # Otherwise, begin setting up WebRTC and acting as a proxy. - dbg 'Contacting Broker at ' + broker.url - snowflake.setRelayAddr config.relayAddr - snowflake.beginWebRTC() - -# Notification of closing tab with active proxy. -window.onbeforeunload = -> - if !silenceNotifications && Snowflake.MODE.WEBRTC_READY == snowflake.state - return Snowflake.MESSAGE.CONFIRMATION - null - -window.onunload = -> - snowflake.disable() - null - -window.onload = init diff --git a/proxy/init-badge.js b/proxy/init-badge.js new file mode 100644 index 0000000..136835b --- /dev/null +++ b/proxy/init-badge.js @@ -0,0 +1,75 @@ +// Generated by CoffeeScript 2.4.1 +/* +Entry point. +*/ +var dbg, debug, init, log, query, silenceNotifications, snowflake; + +if (((typeof TESTING === "undefined" || TESTING === null) || !TESTING) && !Util.featureDetect()) { + console.log('webrtc feature not detected. shutting down'); + return; +} + +snowflake = null; + +query = Query.parse(location); + +debug = Params.getBool(query, 'debug', false); + +silenceNotifications = Params.getBool(query, 'silent', false); + +// Log to both console and UI if applicable. +// Requires that the snowflake and UI objects are hooked up in order to +// log to console. +log = function(msg) { + console.log('Snowflake: ' + msg); + return snowflake != null ? snowflake.ui.log(msg) : void 0; +}; + +dbg = function(msg) { + if (debug || ((snowflake != null ? snowflake.ui : void 0) instanceof DebugUI)) { + return log(msg); + } +}; + +init = function() { + var broker, config, ui; + config = new Config; + if ('off' !== query['ratelimit']) { + config.rateLimitBytes = Params.getByteCount(query, 'ratelimit', config.rateLimitBytes); + } + ui = null; + if (document.getElementById('badge') !== null) { + ui = new BadgeUI(); + } else if (document.getElementById('status') !== null) { + ui = new DebugUI(); + } else { + ui = new UI(); + } + broker = new Broker(config.brokerUrl); + snowflake = new Snowflake(config, ui, broker); + log('== snowflake proxy =='); + if (Util.snowflakeIsDisabled(config.cookieName)) { + // Do not activate the proxy if any number of conditions are true. + log('Currently not active.'); + return; + } + // Otherwise, begin setting up WebRTC and acting as a proxy. + dbg('Contacting Broker at ' + broker.url); + snowflake.setRelayAddr(config.relayAddr); + return snowflake.beginWebRTC(); +}; + +// Notification of closing tab with active proxy. +window.onbeforeunload = function() { + if (!silenceNotifications && Snowflake.MODE.WEBRTC_READY === snowflake.state) { + return Snowflake.MESSAGE.CONFIRMATION; + } + return null; +}; + +window.onunload = function() { + snowflake.disable(); + return null; +}; + +window.onload = init; diff --git a/proxy/init-node.coffee b/proxy/init-node.coffee deleted file mode 100644 index 814e3fc..0000000 --- a/proxy/init-node.coffee +++ /dev/null @@ -1,19 +0,0 @@ -### -Entry point. -### - -config = new Config -ui = new UI() -broker = new Broker config.brokerUrl -snowflake = new Snowflake config, ui, broker - -log = (msg) -> - console.log 'Snowflake: ' + msg - -dbg = log - -log '== snowflake proxy ==' -dbg 'Contacting Broker at ' + broker.url - -snowflake.setRelayAddr config.relayAddr -snowflake.beginWebRTC() diff --git a/proxy/init-node.js b/proxy/init-node.js new file mode 100644 index 0000000..c6e2e52 --- /dev/null +++ b/proxy/init-node.js @@ -0,0 +1,27 @@ +// Generated by CoffeeScript 2.4.1 +/* +Entry point. +*/ +var broker, config, dbg, log, snowflake, ui; + +config = new Config; + +ui = new UI(); + +broker = new Broker(config.brokerUrl); + +snowflake = new Snowflake(config, ui, broker); + +log = function(msg) { + return console.log('Snowflake: ' + msg); +}; + +dbg = log; + +log('== snowflake proxy =='); + +dbg('Contacting Broker at ' + broker.url); + +snowflake.setRelayAddr(config.relayAddr); + +snowflake.beginWebRTC(); diff --git a/proxy/init-webext.coffee b/proxy/init-webext.coffee deleted file mode 100644 index 716604f..0000000 --- a/proxy/init-webext.coffee +++ /dev/null @@ -1,58 +0,0 @@ -### -Entry point. -### - -debug = false -snowflake = null -config = null -broker = null -ui = null - -# Log to both console and UI if applicable. -# Requires that the snowflake and UI objects are hooked up in order to -# log to console. -log = (msg) -> - console.log 'Snowflake: ' + msg - snowflake?.ui.log msg - -dbg = (msg) -> log msg if debug - -if not Util.featureDetect() - chrome.runtime.onConnect.addListener (port) -> - port.postMessage - missingFeature: true - return - -init = () -> - config = new Config - ui = new WebExtUI() - broker = new Broker config.brokerUrl - snowflake = new Snowflake config, ui, broker - - log '== snowflake proxy ==' - ui.initToggle() - -update = () -> - if !ui.enabled - # Do not activate the proxy if any number of conditions are true. - snowflake.disable() - log 'Currently not active.' - return - - # Otherwise, begin setting up WebRTC and acting as a proxy. - dbg 'Contacting Broker at ' + broker.url - log 'Starting snowflake' - snowflake.setRelayAddr config.relayAddr - snowflake.beginWebRTC() - -# Notification of closing tab with active proxy. -window.onbeforeunload = -> - if !silenceNotifications && Snowflake.MODE.WEBRTC_READY == snowflake.state - return Snowflake.MESSAGE.CONFIRMATION - null - -window.onunload = -> - snowflake.disable() - null - -window.onload = init diff --git a/proxy/init-webext.js b/proxy/init-webext.js new file mode 100644 index 0000000..4b23403 --- /dev/null +++ b/proxy/init-webext.js @@ -0,0 +1,76 @@ +// Generated by CoffeeScript 2.4.1 +/* +Entry point. +*/ +var broker, config, dbg, debug, init, log, snowflake, ui, update; + +debug = false; + +snowflake = null; + +config = null; + +broker = null; + +ui = null; + +// Log to both console and UI if applicable. +// Requires that the snowflake and UI objects are hooked up in order to +// log to console. +log = function(msg) { + console.log('Snowflake: ' + msg); + return snowflake != null ? snowflake.ui.log(msg) : void 0; +}; + +dbg = function(msg) { + if (debug) { + return log(msg); + } +}; + +if (!Util.featureDetect()) { + chrome.runtime.onConnect.addListener(function(port) { + return port.postMessage({ + missingFeature: true + }); + }); + return; +} + +init = function() { + config = new Config; + ui = new WebExtUI(); + broker = new Broker(config.brokerUrl); + snowflake = new Snowflake(config, ui, broker); + log('== snowflake proxy =='); + return ui.initToggle(); +}; + +update = function() { + if (!ui.enabled) { + // Do not activate the proxy if any number of conditions are true. + snowflake.disable(); + log('Currently not active.'); + return; + } + // Otherwise, begin setting up WebRTC and acting as a proxy. + dbg('Contacting Broker at ' + broker.url); + log('Starting snowflake'); + snowflake.setRelayAddr(config.relayAddr); + return snowflake.beginWebRTC(); +}; + +// Notification of closing tab with active proxy. +window.onbeforeunload = function() { + if (!silenceNotifications && Snowflake.MODE.WEBRTC_READY === snowflake.state) { + return Snowflake.MESSAGE.CONFIRMATION; + } + return null; +}; + +window.onunload = function() { + snowflake.disable(); + return null; +}; + +window.onload = init; diff --git a/proxy/proxypair.coffee b/proxy/proxypair.coffee deleted file mode 100644 index b66ba4f..0000000 --- a/proxy/proxypair.coffee +++ /dev/null @@ -1,185 +0,0 @@ -### -Represents a single: - - client <-- webrtc --> snowflake <-- websocket --> relay - -Every ProxyPair has a Snowflake ID, which is necessary when responding to the -Broker with an WebRTC answer. -### - -class ProxyPair - MAX_BUFFER: 10 * 1024 * 1024 - pc: null - client: null # WebRTC Data channel - relay: null # websocket - timer: 0 - running: true - active: false # Whether serving a client. - flush_timeout_id: null - onCleanup: null - id: null - - ### - Constructs a ProxyPair where: - - @relayAddr is the destination relay - - @rateLimit specifies a rate limit on traffic - ### - constructor: (@relayAddr, @rateLimit, @pcConfig) -> - @id = Util.genSnowflakeID() - @c2rSchedule = [] - @r2cSchedule = [] - - # Prepare a WebRTC PeerConnection and await for an SDP offer. - begin: -> - @pc = new PeerConnection @pcConfig, { - optional: [ - { DtlsSrtpKeyAgreement: true } - { RtpDataChannels: false } - ] } - @pc.onicecandidate = (evt) => - # Browser sends a null candidate once the ICE gathering completes. - if null == evt.candidate - # TODO: Use a promise.all to tell Snowflake about all offers at once, - # once multiple proxypairs are supported. - dbg 'Finished gathering ICE candidates.' - snowflake.broker.sendAnswer @id, @pc.localDescription - # OnDataChannel triggered remotely from the client when connection succeeds. - @pc.ondatachannel = (dc) => - channel = dc.channel - dbg 'Data Channel established...' - @prepareDataChannel channel - @client = channel - - receiveWebRTCOffer: (offer) -> - if 'offer' != offer.type - log 'Invalid SDP received -- was not an offer.' - return false - try - err = @pc.setRemoteDescription offer - catch e - log 'Invalid SDP message.' - return false - dbg 'SDP ' + offer.type + ' successfully received.' - true - - # Given a WebRTC DataChannel, prepare callbacks. - prepareDataChannel: (channel) => - channel.onopen = => - log 'WebRTC DataChannel opened!' - snowflake.state = Snowflake.MODE.WEBRTC_READY - snowflake.ui.setActive true - # This is the point when the WebRTC datachannel is done, so the next step - # is to establish websocket to the server. - @connectRelay() - channel.onclose = => - log 'WebRTC DataChannel closed.' - snowflake.ui.setStatus 'disconnected by webrtc.' - snowflake.ui.setActive false - snowflake.state = Snowflake.MODE.INIT - @flush() - @close() - channel.onerror = -> log 'Data channel error!' - channel.binaryType = "arraybuffer" - channel.onmessage = @onClientToRelayMessage - - # Assumes WebRTC datachannel is connected. - connectRelay: => - dbg 'Connecting to relay...' - - # Get a remote IP address from the PeerConnection, if possible. Add it to - # the WebSocket URL's query string if available. - # MDN marks remoteDescription as "experimental". However the other two - # options, currentRemoteDescription and pendingRemoteDescription, which - # are not marked experimental, were undefined when I tried them in Firefox - # 52.2.0. - # https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/remoteDes... - 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/remoteDes... + 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@example.com (Jane Doe) - c=IN IP4 224.2.17.12/127 - t=2873397496 2873404696 - a=recvonly - m=audio 49170 RTP/AVP 0 - m=video 51372 RTP/AVP 99 - a=rtpmap:99 h263-1998/90000 - """ - expected: '224.2.17.12' - , - # Missing c= line - sdp: """ - v=0 - o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5 - s=SDP Seminar - i=A Seminar on the session description protocol - u=http://www.example.com/seminars/sdp.pdf - e=j.doe@example.com (Jane Doe) - t=2873397496 2873404696 - a=recvonly - m=audio 49170 RTP/AVP 0 - m=video 51372 RTP/AVP 99 - a=rtpmap:99 h263-1998/90000 - """ - expected: undefined - , - # Single line, IP address only - sdp: "c=IN IP4 224.2.1.1\n" - expected: '224.2.1.1' - , - # Same, with TTL - sdp: "c=IN IP4 224.2.1.1/127\n" - expected: '224.2.1.1' - , - # Same, with TTL and multicast addresses - sdp: "c=IN IP4 224.2.1.1/127/3\n" - expected: '224.2.1.1' - , - # IPv6, address only - sdp: "c=IN IP6 FF15::101\n" - expected: 'ff15::101' - , - # Same, with multicast addresses - sdp: "c=IN IP6 FF15::101/3\n" - expected: 'ff15::101' - , - # Multiple c= lines - sdp: """ - c=IN IP4 1.2.3.4 - c=IN IP4 5.6.7.8 - """ - expected: '1.2.3.4' - , - # Modified from SDP sent by snowflake-client. - # coffeelint: disable - sdp: """ - v=0 - o=- 7860378660295630295 2 IN IP4 127.0.0.1 - s=- - t=0 0 - a=group:BUNDLE data - a=msid-semantic: WMS - m=application 54653 DTLS/SCTP 5000 - c=IN IP4 1.2.3.4 - a=candidate:3581707038 1 udp 2122260223 192.168.0.1 54653 typ host generation 0 network-id 1 network-cost 50 - a=candidate:2617212910 1 tcp 1518280447 192.168.0.1 59673 typ host tcptype passive generation 0 network-id 1 network-cost 50 - a=candidate:2082671819 1 udp 1686052607 1.2.3.4 54653 typ srflx raddr 192.168.0.1 rport 54653 generation 0 network-id 1 network-cost 50 - a=ice-ufrag:IBdf - a=ice-pwd:G3lTrrC9gmhQx481AowtkhYz - a=fingerprint:sha-256 53:F8:84:D9:3C:1F:A0:44:AA:D6:3C:65:80:D3:CB:6F:23:90:17:41:06:F9:9C:10:D8:48:4A:A8:B6:FA:14:A1 - a=setup:actpass - a=mid:data - a=sctpmap:5000 webrtc-datachannel 1024 - """ - # coffeelint: enable - expected: '1.2.3.4' - , - # Improper character within IPv4 - sdp: """ - c=IN IP4 224.2z.1.1 - """ - expected: undefined - , - # Improper character within IPv6 - sdp: """ - c=IN IP6 ff15:g::101 - """ - expected: undefined - , - # Bogus "IP7" addrtype - sdp: "c=IN IP7 1.2.3.4\n" - expected: undefined - ] - - it 'parses SDP', -> - for test in testCases - # https://tools.ietf.org/html/rfc4566#section-5: "The sequence # CRLF - # (0x0d0a) is used to end a record, although parsers SHOULD be tolerant - # and also accept records terminated with a single newline character." - # We represent the test cases with LF line endings for convenience, and - # test them both that way and with CRLF line endings. - expect(Parse.ipFromSDP(test.sdp)?.toLowerCase()).toEqual(test.expected) - expect( - Parse.ipFromSDP(test.sdp.replace(/\n/, "\r\n"))?.toLowerCase() - ).toEqual(test.expected) - -describe 'query string', -> - it 'should parse correctly', -> - expect Query.parse '' - .toEqual {} - expect Query.parse 'a=b' - .toEqual { a: 'b' } - expect Query.parse 'a=b=c' - .toEqual { a: 'b=c' } - expect Query.parse 'a=b&c=d' - .toEqual { a: 'b', c: 'd' } - expect Query.parse 'client=&relay=1.2.3.4%3A9001' - .toEqual { client: '', relay: '1.2.3.4:9001' } - expect Query.parse 'a=b%26c=d' - .toEqual { a: 'b&c=d' } - expect Query.parse 'a%3db=d' - .toEqual { 'a=b': 'd' } - expect Query.parse 'a=b+c%20d' - .toEqual { 'a': 'b c d' } - expect Query.parse 'a=b+c%2bd' - .toEqual { 'a': 'b c+d' } - expect Query.parse 'a+b=c' - .toEqual { 'a b': 'c' } - expect Query.parse 'a=b+c+d' - .toEqual { a: 'b c d' } - it 'uses the first appearance of duplicate key', -> - expect Query.parse 'a=b&c=d&a=e' - .toEqual { a: 'b', c: 'd' } - expect Query.parse 'a' - .toEqual { a: '' } - expect Query.parse '=b' - .toEqual { '': 'b' } - expect Query.parse '&a=b' - .toEqual { '': '', a: 'b' } - expect Query.parse 'a=b&' - .toEqual { a: 'b', '':'' } - expect Query.parse 'a=b&&c=d' - .toEqual { a: 'b', '':'', c: 'd' } - -describe 'Params', -> - describe 'bool', -> - getBool = (query) -> - Params.getBool (Query.parse query), 'param', false - it 'parses correctly', -> - expect(getBool 'param=true').toBe true - expect(getBool 'param').toBe true - expect(getBool 'param=').toBe true - expect(getBool 'param=1').toBe true - expect(getBool 'param=0').toBe false - expect(getBool 'param=false').toBe false - expect(getBool 'param=unexpected').toBeNull() - expect(getBool 'pram=true').toBe false - - describe 'address', -> - DEFAULT = { host: '1.1.1.1', port: 2222 } - getAddress = (query) -> - Params.getAddress query, 'addr', DEFAULT - it 'parses correctly', -> - expect(getAddress {}).toEqual DEFAULT - expect getAddress { addr: '3.3.3.3:4444' } - .toEqual { host: '3.3.3.3', port: 4444 } - expect getAddress { x: '3.3.3.3:4444' } - .toEqual DEFAULT - expect getAddress { addr: '---' } - .toBeNull() diff --git a/proxy/spec/util.spec.js b/proxy/spec/util.spec.js new file mode 100644 index 0000000..65f2324 --- /dev/null +++ b/proxy/spec/util.spec.js @@ -0,0 +1,254 @@ +// Generated by CoffeeScript 2.4.1 +/* +jasmine tests for Snowflake utils +*/ +describe('Parse', function() { + describe('cookie', function() { + return it('parses correctly', function() { + expect(Parse.cookie('')).toEqual({}); + expect(Parse.cookie('a=b')).toEqual({ + a: 'b' + }); + expect(Parse.cookie('a=b=c')).toEqual({ + a: 'b=c' + }); + expect(Parse.cookie('a=b; c=d')).toEqual({ + a: 'b', + c: 'd' + }); + expect(Parse.cookie('a=b ; c=d')).toEqual({ + a: 'b', + c: 'd' + }); + expect(Parse.cookie('a= b')).toEqual({ + a: 'b' + }); + expect(Parse.cookie('a=')).toEqual({ + a: '' + }); + expect(Parse.cookie('key')).toBeNull(); + expect(Parse.cookie('key=%26%20')).toEqual({ + key: '& ' + }); + return expect(Parse.cookie('a=''')).toEqual({ + a: '''' + }); + }); + }); + describe('address', function() { + it('parses IPv4', function() { + expect(Parse.address('')).toBeNull(); + expect(Parse.address('3.3.3.3:4444')).toEqual({ + host: '3.3.3.3', + port: 4444 + }); + expect(Parse.address('3.3.3.3')).toBeNull(); + expect(Parse.address('3.3.3.3:0x1111')).toBeNull(); + expect(Parse.address('3.3.3.3:-4444')).toBeNull(); + return expect(Parse.address('3.3.3.3:65536')).toBeNull(); + }); + return it('parses IPv6', function() { + expect(Parse.address('[1:2::a:f]:4444')).toEqual({ + host: '1:2::a:f', + port: 4444 + }); + expect(Parse.address('[1:2::a:f]')).toBeNull(); + expect(Parse.address('[1:2::a:f]:0x1111')).toBeNull(); + expect(Parse.address('[1:2::a:f]:-4444')).toBeNull(); + expect(Parse.address('[1:2::a:f]:65536')).toBeNull(); + return expect(Parse.address('[1:2::ffff:1.2.3.4]:4444')).toEqual({ + host: '1:2::ffff:1.2.3.4', + port: 4444 + }); + }); + }); + return describe('ipFromSDP', function() { + var testCases; + testCases = [ + { + // https://tools.ietf.org/html/rfc4566#section-5 + sdp: "v=0\no=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\ns=SDP Seminar\ni=A Seminar on the session description protocol\nu=http://www.example.com/seminars/sdp.pdf%5Cne=j.doe@example.com (Jane Doe)\nc=IN IP4 224.2.17.12/127\nt=2873397496 2873404696\na=recvonly\nm=audio 49170 RTP/AVP 0\nm=video 51372 RTP/AVP 99\na=rtpmap:99 h263-1998/90000", + expected: '224.2.17.12' + }, + { + // Missing c= line + sdp: "v=0\no=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\ns=SDP Seminar\ni=A Seminar on the session description protocol\nu=http://www.example.com/seminars/sdp.pdf%5Cne=j.doe@example.com (Jane Doe)\nt=2873397496 2873404696\na=recvonly\nm=audio 49170 RTP/AVP 0\nm=video 51372 RTP/AVP 99\na=rtpmap:99 h263-1998/90000", + expected: void 0 + }, + { + // Single line, IP address only + sdp: "c=IN IP4 224.2.1.1\n", + expected: '224.2.1.1' + }, + { + // Same, with TTL + sdp: "c=IN IP4 224.2.1.1/127\n", + expected: '224.2.1.1' + }, + { + // Same, with TTL and multicast addresses + sdp: "c=IN IP4 224.2.1.1/127/3\n", + expected: '224.2.1.1' + }, + { + // IPv6, address only + sdp: "c=IN IP6 FF15::101\n", + expected: 'ff15::101' + }, + { + // Same, with multicast addresses + sdp: "c=IN IP6 FF15::101/3\n", + expected: 'ff15::101' + }, + { + // Multiple c= lines + sdp: "c=IN IP4 1.2.3.4\nc=IN IP4 5.6.7.8", + expected: '1.2.3.4' + }, + { + // Modified from SDP sent by snowflake-client. + // coffeelint: disable + sdp: "v=0\no=- 7860378660295630295 2 IN IP4 127.0.0.1\ns=-\nt=0 0\na=group:BUNDLE data\na=msid-semantic: WMS\nm=application 54653 DTLS/SCTP 5000\nc=IN IP4 1.2.3.4\na=candidate:3581707038 1 udp 2122260223 192.168.0.1 54653 typ host generation 0 network-id 1 network-cost 50\na=candidate:2617212910 1 tcp 1518280447 192.168.0.1 59673 typ host tcptype passive generation 0 network-id 1 network-cost 50\na=candidate:2082671819 1 udp 1686052607 1.2.3.4 54653 typ srflx raddr 192.168.0.1 rport 54653 generation 0 network-id 1 network-cost 50\na=ice-ufrag:IBdf\na=ice-pwd:G3lTrrC9gmhQx481AowtkhYz\na=fingerprint:sha-256 53:F8:84:D9:3C:1F:A0:44:AA:D6:3C:65:80:D3:CB:6F:23:90:17:41:06:F9:9C:10:D8:48:4A:A8:B6:FA:14:A1\na=setup:actpass\na=mid:data\na=sctpmap:5000 webrtc-datachannel 1024", + // coffeelint: enable + expected: '1.2.3.4' + }, + { + // Improper character within IPv4 + sdp: "c=IN IP4 224.2z.1.1", + expected: void 0 + }, + { + // Improper character within IPv6 + sdp: "c=IN IP6 ff15:g::101", + expected: void 0 + }, + { + // Bogus "IP7" addrtype + sdp: "c=IN IP7 1.2.3.4\n", + expected: void 0 + } + ]; + return it('parses SDP', function() { + var i, len, ref, ref1, results, test; + results = []; + for (i = 0, len = testCases.length; i < len; i++) { + test = testCases[i]; + // https://tools.ietf.org/html/rfc4566#section-5: "The sequence # CRLF + // (0x0d0a) is used to end a record, although parsers SHOULD be tolerant + // and also accept records terminated with a single newline character." + // We represent the test cases with LF line endings for convenience, and + // test them both that way and with CRLF line endings. + expect((ref = Parse.ipFromSDP(test.sdp)) != null ? ref.toLowerCase() : void 0).toEqual(test.expected); + results.push(expect((ref1 = Parse.ipFromSDP(test.sdp.replace(/\n/, "\r\n"))) != null ? ref1.toLowerCase() : void 0).toEqual(test.expected)); + } + return results; + }); + }); +}); + +describe('query string', function() { + it('should parse correctly', function() { + expect(Query.parse('')).toEqual({}); + expect(Query.parse('a=b')).toEqual({ + a: 'b' + }); + expect(Query.parse('a=b=c')).toEqual({ + a: 'b=c' + }); + expect(Query.parse('a=b&c=d')).toEqual({ + a: 'b', + c: 'd' + }); + expect(Query.parse('client=&relay=1.2.3.4%3A9001')).toEqual({ + client: '', + relay: '1.2.3.4:9001' + }); + expect(Query.parse('a=b%26c=d')).toEqual({ + a: 'b&c=d' + }); + expect(Query.parse('a%3db=d')).toEqual({ + 'a=b': 'd' + }); + expect(Query.parse('a=b+c%20d')).toEqual({ + 'a': 'b c d' + }); + expect(Query.parse('a=b+c%2bd')).toEqual({ + 'a': 'b c+d' + }); + expect(Query.parse('a+b=c')).toEqual({ + 'a b': 'c' + }); + return expect(Query.parse('a=b+c+d')).toEqual({ + a: 'b c d' + }); + }); + return it('uses the first appearance of duplicate key', function() { + expect(Query.parse('a=b&c=d&a=e')).toEqual({ + a: 'b', + c: 'd' + }); + expect(Query.parse('a')).toEqual({ + a: '' + }); + expect(Query.parse('=b')).toEqual({ + '': 'b' + }); + expect(Query.parse('&a=b')).toEqual({ + '': '', + a: 'b' + }); + expect(Query.parse('a=b&')).toEqual({ + a: 'b', + '': '' + }); + return expect(Query.parse('a=b&&c=d')).toEqual({ + a: 'b', + '': '', + c: 'd' + }); + }); +}); + +describe('Params', function() { + describe('bool', function() { + var getBool; + getBool = function(query) { + return Params.getBool(Query.parse(query), 'param', false); + }; + return it('parses correctly', function() { + expect(getBool('param=true')).toBe(true); + expect(getBool('param')).toBe(true); + expect(getBool('param=')).toBe(true); + expect(getBool('param=1')).toBe(true); + expect(getBool('param=0')).toBe(false); + expect(getBool('param=false')).toBe(false); + expect(getBool('param=unexpected')).toBeNull(); + return expect(getBool('pram=true')).toBe(false); + }); + }); + return describe('address', function() { + var DEFAULT, getAddress; + DEFAULT = { + host: '1.1.1.1', + port: 2222 + }; + getAddress = function(query) { + return Params.getAddress(query, 'addr', DEFAULT); + }; + return it('parses correctly', function() { + expect(getAddress({})).toEqual(DEFAULT); + expect(getAddress({ + addr: '3.3.3.3:4444' + })).toEqual({ + host: '3.3.3.3', + port: 4444 + }); + expect(getAddress({ + x: '3.3.3.3:4444' + })).toEqual(DEFAULT); + return expect(getAddress({ + addr: '---' + })).toBeNull(); + }); + }); +}); diff --git a/proxy/spec/websocket.spec.coffee b/proxy/spec/websocket.spec.coffee deleted file mode 100644 index 3818e2a..0000000 --- a/proxy/spec/websocket.spec.coffee +++ /dev/null @@ -1,39 +0,0 @@ -### -jasmine tests for Snowflake websocket -### - -describe 'BuildUrl', -> - it 'should parse just protocol and host', -> - expect(WS.buildUrl('http', 'example.com')).toBe 'http://example.com' - it 'should handle different ports', -> - expect WS.buildUrl 'http', 'example.com', 80 - .toBe 'http://example.com' - expect WS.buildUrl 'http', 'example.com', 81 - .toBe 'http://example.com:81' - expect WS.buildUrl 'http', 'example.com', 443 - .toBe 'http://example.com:443' - expect WS.buildUrl 'http', 'example.com', 444 - .toBe 'http://example.com:444' - it 'should handle paths', -> - expect WS.buildUrl 'http', 'example.com', 80, '/' - .toBe 'http://example.com/' - expect WS.buildUrl 'http', 'example.com', 80,'/test?k=%#v' - .toBe 'http://example.com/test%3Fk%3D%25%23v' - expect WS.buildUrl 'http', 'example.com', 80, '/test' - .toBe 'http://example.com/test' - it 'should handle params', -> - expect WS.buildUrl 'http', 'example.com', 80, '/test', [['k', '%#v']] - .toBe 'http://example.com/test?k=%25%23v' - expect(WS.buildUrl( - 'http', 'example.com', 80, '/test', [['a', 'b'], ['c', 'd']] - )).toBe 'http://example.com/test?a=b&c=d' - it 'should handle ips', -> - expect WS.buildUrl 'http', '1.2.3.4' - .toBe 'http://1.2.3.4' - expect WS.buildUrl 'http', '1:2::3:4' - .toBe 'http://%5B1: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://%5B1: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);