mirror of
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake.git
synced 2025-10-13 20:11:19 -04:00
Compile coffee files and remove them
With, ./node_modules/.bin/coffee -b -c Cakefile `find . -path ./node_modules -prune -o -name '*.coffee'`
This commit is contained in:
parent
82562fb21d
commit
31ad9566e6
38 changed files with 2277 additions and 1725 deletions
|
@ -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'
|
84
proxy/Cakefile.js
Normal file
84
proxy/Cakefile.js
Normal file
|
@ -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');
|
||||
});
|
|
@ -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
|
126
proxy/broker.js
Normal file
126
proxy/broker.js
Normal file
|
@ -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);
|
|
@ -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'] }
|
||||
]
|
43
proxy/config.js
Normal file
43
proxy/config.js
Normal file
|
@ -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);
|
|
@ -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
|
75
proxy/init-badge.js
Normal file
75
proxy/init-badge.js
Normal file
|
@ -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;
|
|
@ -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()
|
27
proxy/init-node.js
Normal file
27
proxy/init-node.js
Normal file
|
@ -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();
|
|
@ -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
|
76
proxy/init-webext.js
Normal file
76
proxy/init-webext.js
Normal file
|
@ -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;
|
|
@ -1,185 +0,0 @@
|
|||
###
|
||||
Represents a single:
|
||||
|
||||
client <-- webrtc --> snowflake <-- websocket --> relay
|
||||
|
||||
Every ProxyPair has a Snowflake ID, which is necessary when responding to the
|
||||
Broker with an WebRTC answer.
|
||||
###
|
||||
|
||||
class ProxyPair
|
||||
MAX_BUFFER: 10 * 1024 * 1024
|
||||
pc: null
|
||||
client: null # WebRTC Data channel
|
||||
relay: null # websocket
|
||||
timer: 0
|
||||
running: true
|
||||
active: false # Whether serving a client.
|
||||
flush_timeout_id: null
|
||||
onCleanup: null
|
||||
id: null
|
||||
|
||||
###
|
||||
Constructs a ProxyPair where:
|
||||
- @relayAddr is the destination relay
|
||||
- @rateLimit specifies a rate limit on traffic
|
||||
###
|
||||
constructor: (@relayAddr, @rateLimit, @pcConfig) ->
|
||||
@id = Util.genSnowflakeID()
|
||||
@c2rSchedule = []
|
||||
@r2cSchedule = []
|
||||
|
||||
# Prepare a WebRTC PeerConnection and await for an SDP offer.
|
||||
begin: ->
|
||||
@pc = new PeerConnection @pcConfig, {
|
||||
optional: [
|
||||
{ DtlsSrtpKeyAgreement: true }
|
||||
{ RtpDataChannels: false }
|
||||
] }
|
||||
@pc.onicecandidate = (evt) =>
|
||||
# Browser sends a null candidate once the ICE gathering completes.
|
||||
if null == evt.candidate
|
||||
# TODO: Use a promise.all to tell Snowflake about all offers at once,
|
||||
# once multiple proxypairs are supported.
|
||||
dbg 'Finished gathering ICE candidates.'
|
||||
snowflake.broker.sendAnswer @id, @pc.localDescription
|
||||
# OnDataChannel triggered remotely from the client when connection succeeds.
|
||||
@pc.ondatachannel = (dc) =>
|
||||
channel = dc.channel
|
||||
dbg 'Data Channel established...'
|
||||
@prepareDataChannel channel
|
||||
@client = channel
|
||||
|
||||
receiveWebRTCOffer: (offer) ->
|
||||
if 'offer' != offer.type
|
||||
log 'Invalid SDP received -- was not an offer.'
|
||||
return false
|
||||
try
|
||||
err = @pc.setRemoteDescription offer
|
||||
catch e
|
||||
log 'Invalid SDP message.'
|
||||
return false
|
||||
dbg 'SDP ' + offer.type + ' successfully received.'
|
||||
true
|
||||
|
||||
# Given a WebRTC DataChannel, prepare callbacks.
|
||||
prepareDataChannel: (channel) =>
|
||||
channel.onopen = =>
|
||||
log 'WebRTC DataChannel opened!'
|
||||
snowflake.state = Snowflake.MODE.WEBRTC_READY
|
||||
snowflake.ui.setActive true
|
||||
# This is the point when the WebRTC datachannel is done, so the next step
|
||||
# is to establish websocket to the server.
|
||||
@connectRelay()
|
||||
channel.onclose = =>
|
||||
log 'WebRTC DataChannel closed.'
|
||||
snowflake.ui.setStatus 'disconnected by webrtc.'
|
||||
snowflake.ui.setActive false
|
||||
snowflake.state = Snowflake.MODE.INIT
|
||||
@flush()
|
||||
@close()
|
||||
channel.onerror = -> log 'Data channel error!'
|
||||
channel.binaryType = "arraybuffer"
|
||||
channel.onmessage = @onClientToRelayMessage
|
||||
|
||||
# Assumes WebRTC datachannel is connected.
|
||||
connectRelay: =>
|
||||
dbg 'Connecting to relay...'
|
||||
|
||||
# Get a remote IP address from the PeerConnection, if possible. Add it to
|
||||
# the WebSocket URL's query string if available.
|
||||
# MDN marks remoteDescription as "experimental". However the other two
|
||||
# options, currentRemoteDescription and pendingRemoteDescription, which
|
||||
# are not marked experimental, were undefined when I tried them in Firefox
|
||||
# 52.2.0.
|
||||
# https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/remoteDescription
|
||||
peer_ip = Parse.ipFromSDP(@pc.remoteDescription?.sdp)
|
||||
params = []
|
||||
if peer_ip?
|
||||
params.push(["client_ip", peer_ip])
|
||||
|
||||
@relay = WS.makeWebsocket @relayAddr, params
|
||||
@relay.label = 'websocket-relay'
|
||||
@relay.onopen = =>
|
||||
if @timer
|
||||
clearTimeout @timer
|
||||
@timer = 0
|
||||
log @relay.label + ' connected!'
|
||||
snowflake.ui.setStatus 'connected'
|
||||
@relay.onclose = =>
|
||||
log @relay.label + ' closed.'
|
||||
snowflake.ui.setStatus 'disconnected.'
|
||||
snowflake.ui.setActive false
|
||||
snowflake.state = Snowflake.MODE.INIT
|
||||
@flush()
|
||||
@close()
|
||||
@relay.onerror = @onError
|
||||
@relay.onmessage = @onRelayToClientMessage
|
||||
# TODO: Better websocket timeout handling.
|
||||
@timer = setTimeout((=>
|
||||
return if 0 == @timer
|
||||
log @relay.label + ' timed out connecting.'
|
||||
@relay.onclose()), 5000)
|
||||
|
||||
# WebRTC --> websocket
|
||||
onClientToRelayMessage: (msg) =>
|
||||
dbg 'WebRTC --> websocket data: ' + msg.data.byteLength + ' bytes'
|
||||
@c2rSchedule.push msg.data
|
||||
@flush()
|
||||
|
||||
# websocket --> WebRTC
|
||||
onRelayToClientMessage: (event) =>
|
||||
dbg 'websocket --> WebRTC data: ' + event.data.byteLength + ' bytes'
|
||||
@r2cSchedule.push event.data
|
||||
@flush()
|
||||
|
||||
onError: (event) =>
|
||||
ws = event.target
|
||||
log ws.label + ' error.'
|
||||
@close()
|
||||
|
||||
# Close both WebRTC and websocket.
|
||||
close: ->
|
||||
if @timer
|
||||
clearTimeout @timer
|
||||
@timer = 0
|
||||
@running = false
|
||||
@client.close() if @webrtcIsReady()
|
||||
@relay.close() if @relayIsReady()
|
||||
relay = null
|
||||
@onCleanup()
|
||||
|
||||
# Send as much data in both directions as the rate limit currently allows.
|
||||
flush: =>
|
||||
clearTimeout @flush_timeout_id if @flush_timeout_id
|
||||
@flush_timeout_id = null
|
||||
busy = true
|
||||
checkChunks = =>
|
||||
busy = false
|
||||
# WebRTC --> websocket
|
||||
if @relayIsReady() &&
|
||||
@relay.bufferedAmount < @MAX_BUFFER &&
|
||||
@c2rSchedule.length > 0
|
||||
chunk = @c2rSchedule.shift()
|
||||
@rateLimit.update chunk.byteLength
|
||||
@relay.send chunk
|
||||
busy = true
|
||||
# websocket --> WebRTC
|
||||
if @webrtcIsReady() &&
|
||||
@client.bufferedAmount < @MAX_BUFFER &&
|
||||
@r2cSchedule.length > 0
|
||||
chunk = @r2cSchedule.shift()
|
||||
@rateLimit.update chunk.byteLength
|
||||
@client.send chunk
|
||||
busy = true
|
||||
|
||||
checkChunks() while busy && !@rateLimit.isLimited()
|
||||
|
||||
if @r2cSchedule.length > 0 || @c2rSchedule.length > 0 ||
|
||||
(@relayIsReady() && @relay.bufferedAmount > 0) ||
|
||||
(@webrtcIsReady() && @client.bufferedAmount > 0)
|
||||
@flush_timeout_id = setTimeout @flush, @rateLimit.when() * 1000
|
||||
|
||||
webrtcIsReady: -> null != @client && 'open' == @client.readyState
|
||||
relayIsReady: -> (null != @relay) && (WebSocket.OPEN == @relay.readyState)
|
||||
isClosed: (ws) -> undefined == ws || WebSocket.CLOSED == ws.readyState
|
262
proxy/proxypair.js
Normal file
262
proxy/proxypair.js
Normal file
|
@ -0,0 +1,262 @@
|
|||
// Generated by CoffeeScript 2.4.1
|
||||
/*
|
||||
Represents a single:
|
||||
|
||||
client <-- webrtc --> snowflake <-- websocket --> relay
|
||||
|
||||
Every ProxyPair has a Snowflake ID, which is necessary when responding to the
|
||||
Broker with an WebRTC answer.
|
||||
*/
|
||||
var ProxyPair;
|
||||
|
||||
ProxyPair = (function() {
|
||||
class ProxyPair {
|
||||
/*
|
||||
Constructs a ProxyPair where:
|
||||
- @relayAddr is the destination relay
|
||||
- @rateLimit specifies a rate limit on traffic
|
||||
*/
|
||||
constructor(relayAddr, rateLimit, pcConfig) {
|
||||
// Given a WebRTC DataChannel, prepare callbacks.
|
||||
this.prepareDataChannel = this.prepareDataChannel.bind(this);
|
||||
// Assumes WebRTC datachannel is connected.
|
||||
this.connectRelay = this.connectRelay.bind(this);
|
||||
// WebRTC --> websocket
|
||||
this.onClientToRelayMessage = this.onClientToRelayMessage.bind(this);
|
||||
// websocket --> WebRTC
|
||||
this.onRelayToClientMessage = this.onRelayToClientMessage.bind(this);
|
||||
this.onError = this.onError.bind(this);
|
||||
// Send as much data in both directions as the rate limit currently allows.
|
||||
this.flush = this.flush.bind(this);
|
||||
this.relayAddr = relayAddr;
|
||||
this.rateLimit = rateLimit;
|
||||
this.pcConfig = pcConfig;
|
||||
this.id = Util.genSnowflakeID();
|
||||
this.c2rSchedule = [];
|
||||
this.r2cSchedule = [];
|
||||
}
|
||||
|
||||
// Prepare a WebRTC PeerConnection and await for an SDP offer.
|
||||
begin() {
|
||||
this.pc = new PeerConnection(this.pcConfig, {
|
||||
optional: [
|
||||
{
|
||||
DtlsSrtpKeyAgreement: true
|
||||
},
|
||||
{
|
||||
RtpDataChannels: false
|
||||
}
|
||||
]
|
||||
});
|
||||
this.pc.onicecandidate = (evt) => {
|
||||
// Browser sends a null candidate once the ICE gathering completes.
|
||||
if (null === evt.candidate) {
|
||||
// TODO: Use a promise.all to tell Snowflake about all offers at once,
|
||||
// once multiple proxypairs are supported.
|
||||
dbg('Finished gathering ICE candidates.');
|
||||
return snowflake.broker.sendAnswer(this.id, this.pc.localDescription);
|
||||
}
|
||||
};
|
||||
// OnDataChannel triggered remotely from the client when connection succeeds.
|
||||
return this.pc.ondatachannel = (dc) => {
|
||||
var channel;
|
||||
channel = dc.channel;
|
||||
dbg('Data Channel established...');
|
||||
this.prepareDataChannel(channel);
|
||||
return this.client = channel;
|
||||
};
|
||||
}
|
||||
|
||||
receiveWebRTCOffer(offer) {
|
||||
var e, err;
|
||||
if ('offer' !== offer.type) {
|
||||
log('Invalid SDP received -- was not an offer.');
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
err = this.pc.setRemoteDescription(offer);
|
||||
} catch (error) {
|
||||
e = error;
|
||||
log('Invalid SDP message.');
|
||||
return false;
|
||||
}
|
||||
dbg('SDP ' + offer.type + ' successfully received.');
|
||||
return true;
|
||||
}
|
||||
|
||||
prepareDataChannel(channel) {
|
||||
channel.onopen = () => {
|
||||
log('WebRTC DataChannel opened!');
|
||||
snowflake.state = Snowflake.MODE.WEBRTC_READY;
|
||||
snowflake.ui.setActive(true);
|
||||
// This is the point when the WebRTC datachannel is done, so the next step
|
||||
// is to establish websocket to the server.
|
||||
return this.connectRelay();
|
||||
};
|
||||
channel.onclose = () => {
|
||||
log('WebRTC DataChannel closed.');
|
||||
snowflake.ui.setStatus('disconnected by webrtc.');
|
||||
snowflake.ui.setActive(false);
|
||||
snowflake.state = Snowflake.MODE.INIT;
|
||||
this.flush();
|
||||
return this.close();
|
||||
};
|
||||
channel.onerror = function() {
|
||||
return log('Data channel error!');
|
||||
};
|
||||
channel.binaryType = "arraybuffer";
|
||||
return channel.onmessage = this.onClientToRelayMessage;
|
||||
}
|
||||
|
||||
connectRelay() {
|
||||
var params, peer_ip, ref;
|
||||
dbg('Connecting to relay...');
|
||||
// Get a remote IP address from the PeerConnection, if possible. Add it to
|
||||
// the WebSocket URL's query string if available.
|
||||
// MDN marks remoteDescription as "experimental". However the other two
|
||||
// options, currentRemoteDescription and pendingRemoteDescription, which
|
||||
// are not marked experimental, were undefined when I tried them in Firefox
|
||||
// 52.2.0.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/remoteDescription
|
||||
peer_ip = Parse.ipFromSDP((ref = this.pc.remoteDescription) != null ? ref.sdp : void 0);
|
||||
params = [];
|
||||
if (peer_ip != null) {
|
||||
params.push(["client_ip", peer_ip]);
|
||||
}
|
||||
this.relay = WS.makeWebsocket(this.relayAddr, params);
|
||||
this.relay.label = 'websocket-relay';
|
||||
this.relay.onopen = () => {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = 0;
|
||||
}
|
||||
log(this.relay.label + ' connected!');
|
||||
return snowflake.ui.setStatus('connected');
|
||||
};
|
||||
this.relay.onclose = () => {
|
||||
log(this.relay.label + ' closed.');
|
||||
snowflake.ui.setStatus('disconnected.');
|
||||
snowflake.ui.setActive(false);
|
||||
snowflake.state = Snowflake.MODE.INIT;
|
||||
this.flush();
|
||||
return this.close();
|
||||
};
|
||||
this.relay.onerror = this.onError;
|
||||
this.relay.onmessage = this.onRelayToClientMessage;
|
||||
// TODO: Better websocket timeout handling.
|
||||
return this.timer = setTimeout((() => {
|
||||
if (0 === this.timer) {
|
||||
return;
|
||||
}
|
||||
log(this.relay.label + ' timed out connecting.');
|
||||
return this.relay.onclose();
|
||||
}), 5000);
|
||||
}
|
||||
|
||||
onClientToRelayMessage(msg) {
|
||||
dbg('WebRTC --> websocket data: ' + msg.data.byteLength + ' bytes');
|
||||
this.c2rSchedule.push(msg.data);
|
||||
return this.flush();
|
||||
}
|
||||
|
||||
onRelayToClientMessage(event) {
|
||||
dbg('websocket --> WebRTC data: ' + event.data.byteLength + ' bytes');
|
||||
this.r2cSchedule.push(event.data);
|
||||
return this.flush();
|
||||
}
|
||||
|
||||
onError(event) {
|
||||
var ws;
|
||||
ws = event.target;
|
||||
log(ws.label + ' error.');
|
||||
return this.close();
|
||||
}
|
||||
|
||||
// Close both WebRTC and websocket.
|
||||
close() {
|
||||
var relay;
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = 0;
|
||||
}
|
||||
this.running = false;
|
||||
if (this.webrtcIsReady()) {
|
||||
this.client.close();
|
||||
}
|
||||
if (this.relayIsReady()) {
|
||||
this.relay.close();
|
||||
}
|
||||
relay = null;
|
||||
return this.onCleanup();
|
||||
}
|
||||
|
||||
flush() {
|
||||
var busy, checkChunks;
|
||||
if (this.flush_timeout_id) {
|
||||
clearTimeout(this.flush_timeout_id);
|
||||
}
|
||||
this.flush_timeout_id = null;
|
||||
busy = true;
|
||||
checkChunks = () => {
|
||||
var chunk;
|
||||
busy = false;
|
||||
// WebRTC --> websocket
|
||||
if (this.relayIsReady() && this.relay.bufferedAmount < this.MAX_BUFFER && this.c2rSchedule.length > 0) {
|
||||
chunk = this.c2rSchedule.shift();
|
||||
this.rateLimit.update(chunk.byteLength);
|
||||
this.relay.send(chunk);
|
||||
busy = true;
|
||||
}
|
||||
// websocket --> WebRTC
|
||||
if (this.webrtcIsReady() && this.client.bufferedAmount < this.MAX_BUFFER && this.r2cSchedule.length > 0) {
|
||||
chunk = this.r2cSchedule.shift();
|
||||
this.rateLimit.update(chunk.byteLength);
|
||||
this.client.send(chunk);
|
||||
return busy = true;
|
||||
}
|
||||
};
|
||||
while (busy && !this.rateLimit.isLimited()) {
|
||||
checkChunks();
|
||||
}
|
||||
if (this.r2cSchedule.length > 0 || this.c2rSchedule.length > 0 || (this.relayIsReady() && this.relay.bufferedAmount > 0) || (this.webrtcIsReady() && this.client.bufferedAmount > 0)) {
|
||||
return this.flush_timeout_id = setTimeout(this.flush, this.rateLimit.when() * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
webrtcIsReady() {
|
||||
return null !== this.client && 'open' === this.client.readyState;
|
||||
}
|
||||
|
||||
relayIsReady() {
|
||||
return (null !== this.relay) && (WebSocket.OPEN === this.relay.readyState);
|
||||
}
|
||||
|
||||
isClosed(ws) {
|
||||
return void 0 === ws || WebSocket.CLOSED === ws.readyState;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
ProxyPair.prototype.MAX_BUFFER = 10 * 1024 * 1024;
|
||||
|
||||
ProxyPair.prototype.pc = null;
|
||||
|
||||
ProxyPair.prototype.client = null; // WebRTC Data channel
|
||||
|
||||
ProxyPair.prototype.relay = null; // websocket
|
||||
|
||||
ProxyPair.prototype.timer = 0;
|
||||
|
||||
ProxyPair.prototype.running = true;
|
||||
|
||||
ProxyPair.prototype.active = false; // Whether serving a client.
|
||||
|
||||
ProxyPair.prototype.flush_timeout_id = null;
|
||||
|
||||
ProxyPair.prototype.onCleanup = null;
|
||||
|
||||
ProxyPair.prototype.id = null;
|
||||
|
||||
return ProxyPair;
|
||||
|
||||
}).call(this);
|
|
@ -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
|
34
proxy/shims.js
Normal file
34
proxy/shims.js
Normal file
|
@ -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;
|
||||
}
|
|
@ -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()
|
182
proxy/snowflake.js
Normal file
182
proxy/snowflake.js
Normal file
|
@ -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);
|
|
@ -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()
|
119
proxy/spec/broker.spec.js
Normal file
119
proxy/spec/broker.spec.js
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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
|
34
proxy/spec/init.spec.js
Normal file
34
proxy/spec/init.spec.js
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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
|
143
proxy/spec/proxypair.spec.js
Normal file
143
proxy/spec/proxypair.spec.js
Normal file
|
@ -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
|
|
@ -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
|
114
proxy/spec/snowflake.spec.js
Normal file
114
proxy/spec/snowflake.spec.js
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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
|
84
proxy/spec/ui.spec.js
Normal file
84
proxy/spec/ui.spec.js
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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()
|
254
proxy/spec/util.spec.js
Normal file
254
proxy/spec/util.spec.js
Normal file
|
@ -0,0 +1,254 @@
|
|||
// Generated by CoffeeScript 2.4.1
|
||||
/*
|
||||
jasmine tests for Snowflake utils
|
||||
*/
|
||||
describe('Parse', function() {
|
||||
describe('cookie', function() {
|
||||
return it('parses correctly', function() {
|
||||
expect(Parse.cookie('')).toEqual({});
|
||||
expect(Parse.cookie('a=b')).toEqual({
|
||||
a: 'b'
|
||||
});
|
||||
expect(Parse.cookie('a=b=c')).toEqual({
|
||||
a: 'b=c'
|
||||
});
|
||||
expect(Parse.cookie('a=b; c=d')).toEqual({
|
||||
a: 'b',
|
||||
c: 'd'
|
||||
});
|
||||
expect(Parse.cookie('a=b ; c=d')).toEqual({
|
||||
a: 'b',
|
||||
c: 'd'
|
||||
});
|
||||
expect(Parse.cookie('a= b')).toEqual({
|
||||
a: 'b'
|
||||
});
|
||||
expect(Parse.cookie('a=')).toEqual({
|
||||
a: ''
|
||||
});
|
||||
expect(Parse.cookie('key')).toBeNull();
|
||||
expect(Parse.cookie('key=%26%20')).toEqual({
|
||||
key: '& '
|
||||
});
|
||||
return expect(Parse.cookie('a=\'\'')).toEqual({
|
||||
a: '\'\''
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('address', function() {
|
||||
it('parses IPv4', function() {
|
||||
expect(Parse.address('')).toBeNull();
|
||||
expect(Parse.address('3.3.3.3:4444')).toEqual({
|
||||
host: '3.3.3.3',
|
||||
port: 4444
|
||||
});
|
||||
expect(Parse.address('3.3.3.3')).toBeNull();
|
||||
expect(Parse.address('3.3.3.3:0x1111')).toBeNull();
|
||||
expect(Parse.address('3.3.3.3:-4444')).toBeNull();
|
||||
return expect(Parse.address('3.3.3.3:65536')).toBeNull();
|
||||
});
|
||||
return it('parses IPv6', function() {
|
||||
expect(Parse.address('[1:2::a:f]:4444')).toEqual({
|
||||
host: '1:2::a:f',
|
||||
port: 4444
|
||||
});
|
||||
expect(Parse.address('[1:2::a:f]')).toBeNull();
|
||||
expect(Parse.address('[1:2::a:f]:0x1111')).toBeNull();
|
||||
expect(Parse.address('[1:2::a:f]:-4444')).toBeNull();
|
||||
expect(Parse.address('[1:2::a:f]:65536')).toBeNull();
|
||||
return expect(Parse.address('[1:2::ffff:1.2.3.4]:4444')).toEqual({
|
||||
host: '1:2::ffff:1.2.3.4',
|
||||
port: 4444
|
||||
});
|
||||
});
|
||||
});
|
||||
return describe('ipFromSDP', function() {
|
||||
var testCases;
|
||||
testCases = [
|
||||
{
|
||||
// https://tools.ietf.org/html/rfc4566#section-5
|
||||
sdp: "v=0\no=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\ns=SDP Seminar\ni=A Seminar on the session description protocol\nu=http://www.example.com/seminars/sdp.pdf\ne=j.doe@example.com (Jane Doe)\nc=IN IP4 224.2.17.12/127\nt=2873397496 2873404696\na=recvonly\nm=audio 49170 RTP/AVP 0\nm=video 51372 RTP/AVP 99\na=rtpmap:99 h263-1998/90000",
|
||||
expected: '224.2.17.12'
|
||||
},
|
||||
{
|
||||
// Missing c= line
|
||||
sdp: "v=0\no=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\ns=SDP Seminar\ni=A Seminar on the session description protocol\nu=http://www.example.com/seminars/sdp.pdf\ne=j.doe@example.com (Jane Doe)\nt=2873397496 2873404696\na=recvonly\nm=audio 49170 RTP/AVP 0\nm=video 51372 RTP/AVP 99\na=rtpmap:99 h263-1998/90000",
|
||||
expected: void 0
|
||||
},
|
||||
{
|
||||
// Single line, IP address only
|
||||
sdp: "c=IN IP4 224.2.1.1\n",
|
||||
expected: '224.2.1.1'
|
||||
},
|
||||
{
|
||||
// Same, with TTL
|
||||
sdp: "c=IN IP4 224.2.1.1/127\n",
|
||||
expected: '224.2.1.1'
|
||||
},
|
||||
{
|
||||
// Same, with TTL and multicast addresses
|
||||
sdp: "c=IN IP4 224.2.1.1/127/3\n",
|
||||
expected: '224.2.1.1'
|
||||
},
|
||||
{
|
||||
// IPv6, address only
|
||||
sdp: "c=IN IP6 FF15::101\n",
|
||||
expected: 'ff15::101'
|
||||
},
|
||||
{
|
||||
// Same, with multicast addresses
|
||||
sdp: "c=IN IP6 FF15::101/3\n",
|
||||
expected: 'ff15::101'
|
||||
},
|
||||
{
|
||||
// Multiple c= lines
|
||||
sdp: "c=IN IP4 1.2.3.4\nc=IN IP4 5.6.7.8",
|
||||
expected: '1.2.3.4'
|
||||
},
|
||||
{
|
||||
// Modified from SDP sent by snowflake-client.
|
||||
// coffeelint: disable
|
||||
sdp: "v=0\no=- 7860378660295630295 2 IN IP4 127.0.0.1\ns=-\nt=0 0\na=group:BUNDLE data\na=msid-semantic: WMS\nm=application 54653 DTLS/SCTP 5000\nc=IN IP4 1.2.3.4\na=candidate:3581707038 1 udp 2122260223 192.168.0.1 54653 typ host generation 0 network-id 1 network-cost 50\na=candidate:2617212910 1 tcp 1518280447 192.168.0.1 59673 typ host tcptype passive generation 0 network-id 1 network-cost 50\na=candidate:2082671819 1 udp 1686052607 1.2.3.4 54653 typ srflx raddr 192.168.0.1 rport 54653 generation 0 network-id 1 network-cost 50\na=ice-ufrag:IBdf\na=ice-pwd:G3lTrrC9gmhQx481AowtkhYz\na=fingerprint:sha-256 53:F8:84:D9:3C:1F:A0:44:AA:D6:3C:65:80:D3:CB:6F:23:90:17:41:06:F9:9C:10:D8:48:4A:A8:B6:FA:14:A1\na=setup:actpass\na=mid:data\na=sctpmap:5000 webrtc-datachannel 1024",
|
||||
// coffeelint: enable
|
||||
expected: '1.2.3.4'
|
||||
},
|
||||
{
|
||||
// Improper character within IPv4
|
||||
sdp: "c=IN IP4 224.2z.1.1",
|
||||
expected: void 0
|
||||
},
|
||||
{
|
||||
// Improper character within IPv6
|
||||
sdp: "c=IN IP6 ff15:g::101",
|
||||
expected: void 0
|
||||
},
|
||||
{
|
||||
// Bogus "IP7" addrtype
|
||||
sdp: "c=IN IP7 1.2.3.4\n",
|
||||
expected: void 0
|
||||
}
|
||||
];
|
||||
return it('parses SDP', function() {
|
||||
var i, len, ref, ref1, results, test;
|
||||
results = [];
|
||||
for (i = 0, len = testCases.length; i < len; i++) {
|
||||
test = testCases[i];
|
||||
// https://tools.ietf.org/html/rfc4566#section-5: "The sequence # CRLF
|
||||
// (0x0d0a) is used to end a record, although parsers SHOULD be tolerant
|
||||
// and also accept records terminated with a single newline character."
|
||||
// We represent the test cases with LF line endings for convenience, and
|
||||
// test them both that way and with CRLF line endings.
|
||||
expect((ref = Parse.ipFromSDP(test.sdp)) != null ? ref.toLowerCase() : void 0).toEqual(test.expected);
|
||||
results.push(expect((ref1 = Parse.ipFromSDP(test.sdp.replace(/\n/, "\r\n"))) != null ? ref1.toLowerCase() : void 0).toEqual(test.expected));
|
||||
}
|
||||
return results;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('query string', function() {
|
||||
it('should parse correctly', function() {
|
||||
expect(Query.parse('')).toEqual({});
|
||||
expect(Query.parse('a=b')).toEqual({
|
||||
a: 'b'
|
||||
});
|
||||
expect(Query.parse('a=b=c')).toEqual({
|
||||
a: 'b=c'
|
||||
});
|
||||
expect(Query.parse('a=b&c=d')).toEqual({
|
||||
a: 'b',
|
||||
c: 'd'
|
||||
});
|
||||
expect(Query.parse('client=&relay=1.2.3.4%3A9001')).toEqual({
|
||||
client: '',
|
||||
relay: '1.2.3.4:9001'
|
||||
});
|
||||
expect(Query.parse('a=b%26c=d')).toEqual({
|
||||
a: 'b&c=d'
|
||||
});
|
||||
expect(Query.parse('a%3db=d')).toEqual({
|
||||
'a=b': 'd'
|
||||
});
|
||||
expect(Query.parse('a=b+c%20d')).toEqual({
|
||||
'a': 'b c d'
|
||||
});
|
||||
expect(Query.parse('a=b+c%2bd')).toEqual({
|
||||
'a': 'b c+d'
|
||||
});
|
||||
expect(Query.parse('a+b=c')).toEqual({
|
||||
'a b': 'c'
|
||||
});
|
||||
return expect(Query.parse('a=b+c+d')).toEqual({
|
||||
a: 'b c d'
|
||||
});
|
||||
});
|
||||
return it('uses the first appearance of duplicate key', function() {
|
||||
expect(Query.parse('a=b&c=d&a=e')).toEqual({
|
||||
a: 'b',
|
||||
c: 'd'
|
||||
});
|
||||
expect(Query.parse('a')).toEqual({
|
||||
a: ''
|
||||
});
|
||||
expect(Query.parse('=b')).toEqual({
|
||||
'': 'b'
|
||||
});
|
||||
expect(Query.parse('&a=b')).toEqual({
|
||||
'': '',
|
||||
a: 'b'
|
||||
});
|
||||
expect(Query.parse('a=b&')).toEqual({
|
||||
a: 'b',
|
||||
'': ''
|
||||
});
|
||||
return expect(Query.parse('a=b&&c=d')).toEqual({
|
||||
a: 'b',
|
||||
'': '',
|
||||
c: 'd'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Params', function() {
|
||||
describe('bool', function() {
|
||||
var getBool;
|
||||
getBool = function(query) {
|
||||
return Params.getBool(Query.parse(query), 'param', false);
|
||||
};
|
||||
return it('parses correctly', function() {
|
||||
expect(getBool('param=true')).toBe(true);
|
||||
expect(getBool('param')).toBe(true);
|
||||
expect(getBool('param=')).toBe(true);
|
||||
expect(getBool('param=1')).toBe(true);
|
||||
expect(getBool('param=0')).toBe(false);
|
||||
expect(getBool('param=false')).toBe(false);
|
||||
expect(getBool('param=unexpected')).toBeNull();
|
||||
return expect(getBool('pram=true')).toBe(false);
|
||||
});
|
||||
});
|
||||
return describe('address', function() {
|
||||
var DEFAULT, getAddress;
|
||||
DEFAULT = {
|
||||
host: '1.1.1.1',
|
||||
port: 2222
|
||||
};
|
||||
getAddress = function(query) {
|
||||
return Params.getAddress(query, 'addr', DEFAULT);
|
||||
};
|
||||
return it('parses correctly', function() {
|
||||
expect(getAddress({})).toEqual(DEFAULT);
|
||||
expect(getAddress({
|
||||
addr: '3.3.3.3:4444'
|
||||
})).toEqual({
|
||||
host: '3.3.3.3',
|
||||
port: 4444
|
||||
});
|
||||
expect(getAddress({
|
||||
x: '3.3.3.3:4444'
|
||||
})).toEqual(DEFAULT);
|
||||
return expect(getAddress({
|
||||
addr: '---'
|
||||
})).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,39 +0,0 @@
|
|||
###
|
||||
jasmine tests for Snowflake websocket
|
||||
###
|
||||
|
||||
describe 'BuildUrl', ->
|
||||
it 'should parse just protocol and host', ->
|
||||
expect(WS.buildUrl('http', 'example.com')).toBe 'http://example.com'
|
||||
it 'should handle different ports', ->
|
||||
expect WS.buildUrl 'http', 'example.com', 80
|
||||
.toBe 'http://example.com'
|
||||
expect WS.buildUrl 'http', 'example.com', 81
|
||||
.toBe 'http://example.com:81'
|
||||
expect WS.buildUrl 'http', 'example.com', 443
|
||||
.toBe 'http://example.com:443'
|
||||
expect WS.buildUrl 'http', 'example.com', 444
|
||||
.toBe 'http://example.com:444'
|
||||
it 'should handle paths', ->
|
||||
expect WS.buildUrl 'http', 'example.com', 80, '/'
|
||||
.toBe 'http://example.com/'
|
||||
expect WS.buildUrl 'http', 'example.com', 80,'/test?k=%#v'
|
||||
.toBe 'http://example.com/test%3Fk%3D%25%23v'
|
||||
expect WS.buildUrl 'http', 'example.com', 80, '/test'
|
||||
.toBe 'http://example.com/test'
|
||||
it 'should handle params', ->
|
||||
expect WS.buildUrl 'http', 'example.com', 80, '/test', [['k', '%#v']]
|
||||
.toBe 'http://example.com/test?k=%25%23v'
|
||||
expect(WS.buildUrl(
|
||||
'http', 'example.com', 80, '/test', [['a', 'b'], ['c', 'd']]
|
||||
)).toBe 'http://example.com/test?a=b&c=d'
|
||||
it 'should handle ips', ->
|
||||
expect WS.buildUrl 'http', '1.2.3.4'
|
||||
.toBe 'http://1.2.3.4'
|
||||
expect WS.buildUrl 'http', '1:2::3:4'
|
||||
.toBe 'http://[1:2::3:4]'
|
||||
it 'should handle bogus', ->
|
||||
expect WS.buildUrl 'http', 'bog][us'
|
||||
.toBe 'http://bog%5D%5Bus'
|
||||
expect WS.buildUrl 'http', 'bog:u]s'
|
||||
.toBe 'http://bog%3Au%5Ds'
|
32
proxy/spec/websocket.spec.js
Normal file
32
proxy/spec/websocket.spec.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Generated by CoffeeScript 2.4.1
|
||||
/*
|
||||
jasmine tests for Snowflake websocket
|
||||
*/
|
||||
describe('BuildUrl', function() {
|
||||
it('should parse just protocol and host', function() {
|
||||
return expect(WS.buildUrl('http', 'example.com')).toBe('http://example.com');
|
||||
});
|
||||
it('should handle different ports', function() {
|
||||
expect(WS.buildUrl('http', 'example.com', 80)).toBe('http://example.com');
|
||||
expect(WS.buildUrl('http', 'example.com', 81)).toBe('http://example.com:81');
|
||||
expect(WS.buildUrl('http', 'example.com', 443)).toBe('http://example.com:443');
|
||||
return expect(WS.buildUrl('http', 'example.com', 444)).toBe('http://example.com:444');
|
||||
});
|
||||
it('should handle paths', function() {
|
||||
expect(WS.buildUrl('http', 'example.com', 80, '/')).toBe('http://example.com/');
|
||||
expect(WS.buildUrl('http', 'example.com', 80, '/test?k=%#v')).toBe('http://example.com/test%3Fk%3D%25%23v');
|
||||
return expect(WS.buildUrl('http', 'example.com', 80, '/test')).toBe('http://example.com/test');
|
||||
});
|
||||
it('should handle params', function() {
|
||||
expect(WS.buildUrl('http', 'example.com', 80, '/test', [['k', '%#v']])).toBe('http://example.com/test?k=%25%23v');
|
||||
return expect(WS.buildUrl('http', 'example.com', 80, '/test', [['a', 'b'], ['c', 'd']])).toBe('http://example.com/test?a=b&c=d');
|
||||
});
|
||||
it('should handle ips', function() {
|
||||
expect(WS.buildUrl('http', '1.2.3.4')).toBe('http://1.2.3.4');
|
||||
return expect(WS.buildUrl('http', '1:2::3:4')).toBe('http://[1:2::3:4]');
|
||||
});
|
||||
return it('should handle bogus', function() {
|
||||
expect(WS.buildUrl('http', 'bog][us')).toBe('http://bog%5D%5Bus');
|
||||
return expect(WS.buildUrl('http', 'bog:u]s')).toBe('http://bog%3Au%5Ds');
|
||||
});
|
||||
});
|
125
proxy/ui.coffee
125
proxy/ui.coffee
|
@ -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"
|
197
proxy/ui.js
Normal file
197
proxy/ui.js
Normal file
|
@ -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);
|
|
@ -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
|
321
proxy/util.js
Normal file
321
proxy/util.js
Normal file
|
@ -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;
|
||||
}
|
||||
|
||||
};
|
|
@ -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
|
70
proxy/websocket.js
Normal file
70
proxy/websocket.js
Normal file
|
@ -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);
|
Loading…
Add table
Add a link
Reference in a new issue