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:
Arlo Breault 2019-07-06 15:13:06 +02:00
parent 82562fb21d
commit 31ad9566e6
38 changed files with 2277 additions and 1725 deletions

View file

@ -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
View 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');
});

View file

@ -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
View 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);

View file

@ -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
View 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);

View file

@ -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
View 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;

View file

@ -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
View 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();

View file

@ -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
View 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;

View file

@ -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
View 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);

View file

@ -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
View 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;
}

View file

@ -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
View 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);

View file

@ -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
View 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();
});
});

View file

@ -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
View 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);
});
});

View file

@ -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

View 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

View file

@ -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

View 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);
});
});

View file

@ -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
View 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);
});
});

View file

@ -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
View 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();
});
});
});

View file

@ -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'

View 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');
});
});

View file

@ -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
View 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);

View file

@ -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
View 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;
}
};

View file

@ -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
View 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);