Lightly massage some of the generated JavaScript

This commit is contained in:
Arlo Breault 2019-07-06 15:20:07 +02:00
parent 31ad9566e6
commit 1867a3f121
19 changed files with 986 additions and 989 deletions

View file

@ -1,26 +1,44 @@
// Generated by CoffeeScript 2.4.1
var FILES, FILES_SPEC, INITS, OUTFILE, STATIC, compileCoffee, copyStaticFiles, exec, execSync, fs, spawn;
fs = require('fs'); var fs = require('fs');
var { exec, spawn, execSync } = require('child_process');
({exec, spawn, execSync} = require('child_process'));
// All coffeescript files required. // All coffeescript files required.
FILES = ['broker.coffee', 'config.coffee', 'proxypair.coffee', 'snowflake.coffee', 'ui.coffee', 'util.coffee', 'websocket.coffee', 'shims.coffee']; var 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']; var 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']; var 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'; var OUTFILE = 'snowflake.js';
STATIC = 'static'; var STATIC = 'static';
copyStaticFiles = function() { var copyStaticFiles = function() {
return exec('cp ' + STATIC + '/* build/'); return exec('cp ' + STATIC + '/* build/');
}; };
compileCoffee = function(outDir, init) { var compileCoffee = function(outDir, init) {
var files; var files;
files = FILES.concat('init-' + init + '.coffee'); files = FILES.concat('init-' + init + '.coffee');
return exec('cat ' + files.join(' ') + ' | coffee -cs > ' + outDir + '/' + OUTFILE, function(err, stdout, stderr) { return exec('cat ' + files.join(' ') + ' | coffee -cs > ' + outDir + '/' + OUTFILE, function(err, stdout, stderr) {

View file

@ -1,73 +1,44 @@
// Generated by CoffeeScript 2.4.1
/* /*
Communication with the snowflake broker. Communication with the snowflake broker.
Browser snowflakes must register with the broker in order Browser snowflakes must register with the broker in order
to get assigned to clients. to get assigned to clients.
*/ */
var Broker;
Broker = (function() { // Represents a broker running remotely.
// Represents a broker running remotely. class Broker {
class Broker {
// When interacting with the Broker, snowflake must generate a unique session // When interacting with the Broker, snowflake must generate a unique session
// ID so the Broker can keep track of each proxy's signalling channels. // ID so the Broker can keep track of each proxy's signalling channels.
// On construction, this Broker object does not do anything until // On construction, this Broker object does not do anything until
// |getClientOffer| is called. // |getClientOffer| is called.
constructor(url) { constructor(url) {
// Promises some client SDP Offer. // Promises some client SDP Offer.
// Registers this Snowflake with the broker using an HTTP POST request, and // Registers this Snowflake with the broker using an HTTP POST request, and
// waits for a response containing some client offer that the Broker chooses // waits for a response containing some client offer that the Broker chooses
// for this proxy.. // for this proxy..
// TODO: Actually support multiple clients. // TODO: Actually support multiple clients.
this.getClientOffer = this.getClientOffer.bind(this); this.getClientOffer = this.getClientOffer.bind(this);
// urlSuffix for the broker is different depending on what action // urlSuffix for the broker is different depending on what action
// is desired. // is desired.
this._postRequest = this._postRequest.bind(this); this._postRequest = this._postRequest.bind(this);
this.url = url; this.url = url;
this.clients = 0; this.clients = 0;
if (0 === this.url.indexOf('localhost', 0)) { if (0 === this.url.indexOf('localhost', 0)) {
// Ensure url has the right protocol + trailing slash. // Ensure url has the right protocol + trailing slash.
this.url = 'http://' + this.url; this.url = 'http://' + this.url;
}
if (0 !== this.url.indexOf('http', 0)) {
this.url = 'https://' + this.url;
}
if ('/' !== this.url.substr(-1)) {
this.url += '/';
}
} }
if (0 !== this.url.indexOf('http', 0)) {
getClientOffer(id) { this.url = 'https://' + this.url;
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);
});
} }
if ('/' !== this.url.substr(-1)) {
this.url += '/';
}
}
// Assumes getClientOffer happened, and a WebRTC SDP answer has been generated. getClientOffer(id) {
// Sends it back to the broker, which passes it to back to the original client. return new Promise((fulfill, reject) => {
sendAnswer(id, answer) {
var xhr; var xhr;
dbg(id + ' - Sending answer back to broker...\n');
dbg(answer.sdp);
xhr = new XMLHttpRequest(); xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() { xhr.onreadystatechange = function() {
if (xhr.DONE !== xhr.readyState) { if (xhr.DONE !== xhr.readyState) {
@ -75,52 +46,75 @@ Broker = (function() {
} }
switch (xhr.status) { switch (xhr.status) {
case Broker.STATUS.OK: case Broker.STATUS.OK:
dbg('Broker: Successfully replied with answer.'); return fulfill(xhr.responseText); // Should contain offer.
return dbg(xhr.responseText); case Broker.STATUS.GATEWAY_TIMEOUT:
case Broker.STATUS.GONE: return reject(Broker.MESSAGE.TIMEOUT);
return dbg('Broker: No longer valid to reply with answer.');
default: default:
dbg('Broker ERROR: Unexpected ' + xhr.status + ' - ' + xhr.statusText); log('Broker ERROR: Unexpected ' + xhr.status + ' - ' + xhr.statusText);
return snowflake.ui.setStatus(' failure. Please refresh.'); snowflake.ui.setStatus(' failure. Please refresh.');
return reject(Broker.MESSAGE.UNEXPECTED);
} }
}; };
return this._postRequest(id, xhr, 'answer', JSON.stringify(answer)); this._xhr = xhr; // Used by spec to fake async Broker interaction
} return this._postRequest(id, xhr, 'proxy', id);
});
}
_postRequest(id, xhr, urlSuffix, payload) { // Assumes getClientOffer happened, and a WebRTC SDP answer has been generated.
var err; // Sends it back to the broker, which passes it to back to the original client.
try { sendAnswer(id, answer) {
xhr.open('POST', this.url + urlSuffix); var xhr;
xhr.setRequestHeader('X-Session-ID', id); dbg(id + ' - Sending answer back to broker...\n');
} catch (error) { dbg(answer.sdp);
err = error; xhr = new XMLHttpRequest();
/* xhr.onreadystatechange = function() {
An exception happens here when, for example, NoScript allows the domain if (xhr.DONE !== xhr.readyState) {
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;
} }
return xhr.send(payload); 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 = { Broker.STATUS = {
OK: 200, OK: 200,
GONE: 410, GONE: 410,
GATEWAY_TIMEOUT: 504 GATEWAY_TIMEOUT: 504
}; };
Broker.MESSAGE = { Broker.MESSAGE = {
TIMEOUT: 'Timed out waiting for a client offer.', TIMEOUT: 'Timed out waiting for a client offer.',
UNEXPECTED: 'Unexpected status.' UNEXPECTED: 'Unexpected status.'
}; };
Broker.prototype.clients = 0; Broker.prototype.clients = 0;
return Broker;
}).call(this);

View file

@ -1,43 +1,36 @@
// Generated by CoffeeScript 2.4.1
var Config;
Config = (function() { class Config {};
class Config {};
Config.prototype.brokerUrl = 'snowflake-broker.bamsoftware.com'; Config.prototype.brokerUrl = 'snowflake-broker.bamsoftware.com';
Config.prototype.relayAddr = { Config.prototype.relayAddr = {
host: 'snowflake.bamsoftware.com', host: 'snowflake.bamsoftware.com',
port: '443' port: '443'
}; };
// Original non-wss relay: // Original non-wss relay:
// host: '192.81.135.242' // host: '192.81.135.242'
// port: 9902 // port: 9902
Config.prototype.cookieName = "snowflake-allow"; Config.prototype.cookieName = "snowflake-allow";
// Bytes per second. Set to undefined to disable limit. // Bytes per second. Set to undefined to disable limit.
Config.prototype.rateLimitBytes = void 0; Config.prototype.rateLimitBytes = void 0;
Config.prototype.minRateLimit = 10 * 1024; Config.prototype.minRateLimit = 10 * 1024;
Config.prototype.rateLimitHistory = 5.0; Config.prototype.rateLimitHistory = 5.0;
Config.prototype.defaultBrokerPollInterval = 5.0 * 1000; Config.prototype.defaultBrokerPollInterval = 5.0 * 1000;
Config.prototype.maxNumClients = 1; Config.prototype.maxNumClients = 1;
Config.prototype.connectionsPerClient = 1; Config.prototype.connectionsPerClient = 1;
// TODO: Different ICE servers. // TODO: Different ICE servers.
Config.prototype.pcConfig = { Config.prototype.pcConfig = {
iceServers: [ iceServers: [
{ {
urls: ['stun:stun.l.google.com:19302'] urls: ['stun:stun.l.google.com:19302']
} }
] ]
}; };
return Config;
}).call(this);

View file

@ -1,37 +1,35 @@
// Generated by CoffeeScript 2.4.1
/* /*
Entry point. Entry point.
*/ */
var dbg, debug, init, log, query, silenceNotifications, snowflake;
if (((typeof TESTING === "undefined" || TESTING === null) || !TESTING) && !Util.featureDetect()) { if (((typeof TESTING === "undefined" || TESTING === null) || !TESTING) && !Util.featureDetect()) {
console.log('webrtc feature not detected. shutting down'); console.log('webrtc feature not detected. shutting down');
return; return;
} }
snowflake = null; var snowflake = null;
query = Query.parse(location); var query = Query.parse(location);
debug = Params.getBool(query, 'debug', false); var debug = Params.getBool(query, 'debug', false);
silenceNotifications = Params.getBool(query, 'silent', false); var silenceNotifications = Params.getBool(query, 'silent', false);
// Log to both console and UI if applicable. // Log to both console and UI if applicable.
// Requires that the snowflake and UI objects are hooked up in order to // Requires that the snowflake and UI objects are hooked up in order to
// log to console. // log to console.
log = function(msg) { var log = function(msg) {
console.log('Snowflake: ' + msg); console.log('Snowflake: ' + msg);
return snowflake != null ? snowflake.ui.log(msg) : void 0; return snowflake != null ? snowflake.ui.log(msg) : void 0;
}; };
dbg = function(msg) { var dbg = function(msg) {
if (debug || ((snowflake != null ? snowflake.ui : void 0) instanceof DebugUI)) { if (debug || ((snowflake != null ? snowflake.ui : void 0) instanceof DebugUI)) {
return log(msg); return log(msg);
} }
}; };
init = function() { var init = function() {
var broker, config, ui; var broker, config, ui;
config = new Config; config = new Config;
if ('off' !== query['ratelimit']) { if ('off' !== query['ratelimit']) {

View file

@ -1,22 +1,20 @@
// Generated by CoffeeScript 2.4.1
/* /*
Entry point. Entry point.
*/ */
var broker, config, dbg, log, snowflake, ui;
config = new Config; var config = new Config;
ui = new UI(); var ui = new UI();
broker = new Broker(config.brokerUrl); var broker = new Broker(config.brokerUrl);
snowflake = new Snowflake(config, ui, broker); var snowflake = new Snowflake(config, ui, broker);
log = function(msg) { var log = function(msg) {
return console.log('Snowflake: ' + msg); return console.log('Snowflake: ' + msg);
}; };
dbg = log; var dbg = log;
log('== snowflake proxy =='); log('== snowflake proxy ==');

View file

@ -1,28 +1,26 @@
// Generated by CoffeeScript 2.4.1
/* /*
Entry point. Entry point.
*/ */
var broker, config, dbg, debug, init, log, snowflake, ui, update;
debug = false; var debug = false;
snowflake = null; var snowflake = null;
config = null; var config = null;
broker = null; var broker = null;
ui = null; var ui = null;
// Log to both console and UI if applicable. // Log to both console and UI if applicable.
// Requires that the snowflake and UI objects are hooked up in order to // Requires that the snowflake and UI objects are hooked up in order to
// log to console. // log to console.
log = function(msg) { var log = function(msg) {
console.log('Snowflake: ' + msg); console.log('Snowflake: ' + msg);
return snowflake != null ? snowflake.ui.log(msg) : void 0; return snowflake != null ? snowflake.ui.log(msg) : void 0;
}; };
dbg = function(msg) { var dbg = function(msg) {
if (debug) { if (debug) {
return log(msg); return log(msg);
} }
@ -37,7 +35,7 @@ if (!Util.featureDetect()) {
return; return;
} }
init = function() { var init = function() {
config = new Config; config = new Config;
ui = new WebExtUI(); ui = new WebExtUI();
broker = new Broker(config.brokerUrl); broker = new Broker(config.brokerUrl);
@ -46,7 +44,7 @@ init = function() {
return ui.initToggle(); return ui.initToggle();
}; };
update = function() { var update = function() {
if (!ui.enabled) { if (!ui.enabled) {
// Do not activate the proxy if any number of conditions are true. // Do not activate the proxy if any number of conditions are true.
snowflake.disable(); snowflake.disable();

View file

@ -1,4 +1,3 @@
// Generated by CoffeeScript 2.4.1
/* /*
Represents a single: Represents a single:
@ -7,256 +6,251 @@ Represents a single:
Every ProxyPair has a Snowflake ID, which is necessary when responding to the Every ProxyPair has a Snowflake ID, which is necessary when responding to the
Broker with an WebRTC answer. Broker with an WebRTC answer.
*/ */
var ProxyPair;
ProxyPair = (function() { class ProxyPair {
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() { Constructs a ProxyPair where:
this.pc = new PeerConnection(this.pcConfig, { - @relayAddr is the destination relay
optional: [ - @rateLimit specifies a rate limit on traffic
{ */
DtlsSrtpKeyAgreement: true constructor(relayAddr, rateLimit, pcConfig) {
}, // Given a WebRTC DataChannel, prepare callbacks.
{ this.prepareDataChannel = this.prepareDataChannel.bind(this);
RtpDataChannels: false // Assumes WebRTC datachannel is connected.
} this.connectRelay = this.connectRelay.bind(this);
] // WebRTC --> websocket
}); this.onClientToRelayMessage = this.onClientToRelayMessage.bind(this);
this.pc.onicecandidate = (evt) => { // websocket --> WebRTC
// Browser sends a null candidate once the ICE gathering completes. this.onRelayToClientMessage = this.onRelayToClientMessage.bind(this);
if (null === evt.candidate) { this.onError = this.onError.bind(this);
// TODO: Use a promise.all to tell Snowflake about all offers at once, // Send as much data in both directions as the rate limit currently allows.
// once multiple proxypairs are supported. this.flush = this.flush.bind(this);
dbg('Finished gathering ICE candidates.'); this.relayAddr = relayAddr;
return snowflake.broker.sendAnswer(this.id, this.pc.localDescription); 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
} }
}; ]
// OnDataChannel triggered remotely from the client when connection succeeds. });
return this.pc.ondatachannel = (dc) => { this.pc.onicecandidate = (evt) => {
var channel; // Browser sends a null candidate once the ICE gathering completes.
channel = dc.channel; if (null === evt.candidate) {
dbg('Data Channel established...'); // TODO: Use a promise.all to tell Snowflake about all offers at once,
this.prepareDataChannel(channel); // once multiple proxypairs are supported.
return this.client = channel; dbg('Finished gathering ICE candidates.');
}; return snowflake.broker.sendAnswer(this.id, this.pc.localDescription);
}
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); // OnDataChannel triggered remotely from the client when connection succeeds.
} catch (error) { return this.pc.ondatachannel = (dc) => {
e = error; var channel;
log('Invalid SDP message.'); channel = dc.channel;
return false; dbg('Data Channel established...');
} this.prepareDataChannel(channel);
dbg('SDP ' + offer.type + ' successfully received.'); return this.client = channel;
return true; };
} }
prepareDataChannel(channel) { receiveWebRTCOffer(offer) {
channel.onopen = () => { var e, err;
log('WebRTC DataChannel opened!'); if ('offer' !== offer.type) {
snowflake.state = Snowflake.MODE.WEBRTC_READY; log('Invalid SDP received -- was not an offer.');
snowflake.ui.setActive(true); return false;
// 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;
} }
try {
connectRelay() { err = this.pc.setRemoteDescription(offer);
var params, peer_ip, ref; } catch (error) {
dbg('Connecting to relay...'); e = error;
// Get a remote IP address from the PeerConnection, if possible. Add it to log('Invalid SDP message.');
// the WebSocket URL's query string if available. return false;
// 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);
} }
dbg('SDP ' + offer.type + ' successfully received.');
return true;
}
onClientToRelayMessage(msg) { prepareDataChannel(channel) {
dbg('WebRTC --> websocket data: ' + msg.data.byteLength + ' bytes'); channel.onopen = () => {
this.c2rSchedule.push(msg.data); log('WebRTC DataChannel opened!');
return this.flush(); snowflake.state = Snowflake.MODE.WEBRTC_READY;
} snowflake.ui.setActive(true);
// This is the point when the WebRTC datachannel is done, so the next step
onRelayToClientMessage(event) { // is to establish websocket to the server.
dbg('websocket --> WebRTC data: ' + event.data.byteLength + ' bytes'); return this.connectRelay();
this.r2cSchedule.push(event.data); };
return this.flush(); channel.onclose = () => {
} log('WebRTC DataChannel closed.');
snowflake.ui.setStatus('disconnected by webrtc.');
onError(event) { snowflake.ui.setActive(false);
var ws; snowflake.state = Snowflake.MODE.INIT;
ws = event.target; this.flush();
log(ws.label + ' error.');
return this.close(); return this.close();
} };
channel.onerror = function() {
return log('Data channel error!');
};
channel.binaryType = "arraybuffer";
return channel.onmessage = this.onClientToRelayMessage;
}
// Close both WebRTC and websocket. connectRelay() {
close() { var params, peer_ip, ref;
var relay; 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) { if (this.timer) {
clearTimeout(this.timer); clearTimeout(this.timer);
this.timer = 0; this.timer = 0;
} }
this.running = false; log(this.relay.label + ' connected!');
if (this.webrtcIsReady()) { return snowflake.ui.setStatus('connected');
this.client.close(); };
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;
} }
if (this.relayIsReady()) { log(this.relay.label + ' timed out connecting.');
this.relay.close(); 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;
} }
relay = null; // websocket --> WebRTC
return this.onCleanup(); if (this.webrtcIsReady() && this.client.bufferedAmount < this.MAX_BUFFER && this.r2cSchedule.length > 0) {
} chunk = this.r2cSchedule.shift();
this.rateLimit.update(chunk.byteLength);
flush() { this.client.send(chunk);
var busy, checkChunks; return busy = true;
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);
} }
};
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)) {
webrtcIsReady() { return this.flush_timeout_id = setTimeout(this.flush, this.rateLimit.when() * 1000);
return null !== this.client && 'open' === this.client.readyState;
} }
}
relayIsReady() { webrtcIsReady() {
return (null !== this.relay) && (WebSocket.OPEN === this.relay.readyState); return null !== this.client && 'open' === this.client.readyState;
} }
isClosed(ws) { relayIsReady() {
return void 0 === ws || WebSocket.CLOSED === ws.readyState; 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.MAX_BUFFER = 10 * 1024 * 1024;
ProxyPair.prototype.client = null; // WebRTC Data channel ProxyPair.prototype.pc = null;
ProxyPair.prototype.relay = null; // websocket ProxyPair.prototype.client = null; // WebRTC Data channel
ProxyPair.prototype.timer = 0; ProxyPair.prototype.relay = null; // websocket
ProxyPair.prototype.running = true; ProxyPair.prototype.timer = 0;
ProxyPair.prototype.active = false; // Whether serving a client. ProxyPair.prototype.running = true;
ProxyPair.prototype.flush_timeout_id = null; ProxyPair.prototype.active = false; // Whether serving a client.
ProxyPair.prototype.onCleanup = null; ProxyPair.prototype.flush_timeout_id = null;
ProxyPair.prototype.id = null; ProxyPair.prototype.onCleanup = null;
return ProxyPair; ProxyPair.prototype.id = null;
}).call(this);

View file

@ -1,8 +1,6 @@
// Generated by CoffeeScript 2.4.1
/* /*
WebRTC shims for multiple browsers. 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) { if (typeof module !== "undefined" && module !== null ? module.exports : void 0) {
window = {}; window = {};
@ -19,10 +17,9 @@ if (typeof module !== "undefined" && module !== null ? module.exports : void 0)
IceCandidate = webrtc.RTCIceCandidate; IceCandidate = webrtc.RTCIceCandidate;
SessionDescription = webrtc.RTCSessionDescription; SessionDescription = webrtc.RTCSessionDescription;
WebSocket = require('ws'); WebSocket = require('ws');
({XMLHttpRequest} = require('xmlhttprequest')); ({ XMLHttpRequest } = require('xmlhttprequest'));
} }
} else { } else {
window = this;
document = window.document; document = window.document;
chrome = window.chrome; chrome = window.chrome;
location = window.location.search.substr(1); location = window.location.search.substr(1);

View file

@ -1,4 +1,3 @@
// Generated by CoffeeScript 2.4.1
/* /*
A Coffeescript WebRTC snowflake proxy A Coffeescript WebRTC snowflake proxy
@ -9,174 +8,169 @@ this proxy must always act as the answerer.
TODO: More documentation TODO: More documentation
*/ */
var Snowflake;
Snowflake = (function() { // Minimum viable snowflake for now - just 1 client.
// Minimum viable snowflake for now - just 1 client. class Snowflake {
class Snowflake {
// Prepare the Snowflake with a Broker (to find clients) and optional UI. // Prepare the Snowflake with a Broker (to find clients) and optional UI.
constructor(config, ui, broker) { constructor(config, ui, broker) {
// Receive an SDP offer from some client assigned by the Broker, // Receive an SDP offer from some client assigned by the Broker,
// |pair| - an available ProxyPair. // |pair| - an available ProxyPair.
this.receiveOffer = this.receiveOffer.bind(this); this.receiveOffer = this.receiveOffer.bind(this);
this.config = config; this.config = config;
this.ui = ui; this.ui = ui;
this.broker = broker; this.broker = broker;
this.state = Snowflake.MODE.INIT; this.state = Snowflake.MODE.INIT;
this.proxyPairs = []; this.proxyPairs = [];
if (void 0 === this.config.rateLimitBytes) { if (void 0 === this.config.rateLimitBytes) {
this.rateLimit = new DummyRateLimit(); this.rateLimit = new DummyRateLimit();
} else { } else {
this.rateLimit = new BucketRateLimit(this.config.rateLimitBytes * this.config.rateLimitHistory, this.config.rateLimitHistory); this.rateLimit = new BucketRateLimit(this.config.rateLimitBytes * this.config.rateLimitHistory, this.config.rateLimitHistory);
}
this.retries = 0;
} }
this.retries = 0;
}
// Set the target relay address spec, which is expected to be websocket. // Set the target relay address spec, which is expected to be websocket.
// TODO: Should potentially fetch the target from broker later, or modify // TODO: Should potentially fetch the target from broker later, or modify
// entirely for the Tor-independent version. // entirely for the Tor-independent version.
setRelayAddr(relayAddr) { setRelayAddr(relayAddr) {
this.relayAddr = relayAddr; this.relayAddr = relayAddr;
log('Using ' + relayAddr.host + ':' + relayAddr.port + ' as Relay.'); log('Using ' + relayAddr.host + ':' + relayAddr.port + ' as Relay.');
return true; 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.
// Initialize WebRTC PeerConnection, which requires beginning the signalling pair.active = true;
// process. |pollBroker| automatically arranges signalling. msg = 'Polling for client ... ';
beginWebRTC() { if (this.retries > 0) {
this.state = Snowflake.MODE.WEBRTC_CONNECTING; msg += '[retries: ' + this.retries + ']';
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);
} }
this.ui.setStatus(msg);
// Regularly poll Broker for clients to serve until this snowflake is recv = this.broker.getClientOffer(pair.id);
// serving at capacity, at which point stop polling. recv.then((desc) => {
pollBroker() { if (pair.running) {
var msg, pair, recv; if (!this.receiveOffer(pair, desc)) {
// 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; return pair.active = false;
} }
}, function(err) { } else {
return pair.active = false; 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) { }, function(err) {
return !pp.active; return pair.active = false;
}); });
} return this.retries++;
}
receiveOffer(pair, desc) { // Returns the first ProxyPair that's available to connect.
var e, offer, sdp; nextAvailableProxyPair() {
try { if (this.proxyPairs.length < this.config.connectionsPerClient) {
offer = JSON.parse(desc); return this.makeProxyPair(this.relayAddr);
dbg('Received:\n\n' + offer.sdp + '\n'); }
sdp = new SessionDescription(offer); return this.proxyPairs.find(function(pp, i, arr) {
if (pair.receiveWebRTCOffer(sdp)) { return !pp.active;
this.sendAnswer(pair); });
return true; }
} else {
return false; receiveOffer(pair, desc) {
} var e, offer, sdp;
} catch (error) { try {
e = error; offer = JSON.parse(desc);
log('ERROR: Unable to receive Offer: ' + e); dbg('Received:\n\n' + offer.sdp + '\n');
sdp = new SessionDescription(offer);
if (pair.receiveWebRTCOffer(sdp)) {
this.sendAnswer(pair);
return true;
} else {
return false; return false;
} }
} catch (error) {
e = error;
log('ERROR: Unable to receive Offer: ' + e);
return false;
} }
}
sendAnswer(pair) { sendAnswer(pair) {
var fail, next; var fail, next;
next = function(sdp) { next = function(sdp) {
dbg('webrtc: Answer ready.'); dbg('webrtc: Answer ready.');
return pair.pc.setLocalDescription(sdp); return pair.pc.setLocalDescription(sdp);
}; };
fail = function() { fail = function() {
return dbg('webrtc: Failed to create Answer'); return dbg('webrtc: Failed to create Answer');
}; };
return pair.pc.createAnswer().then(next).catch(fail); return pair.pc.createAnswer().then(next).catch(fail);
} }
makeProxyPair(relay) { makeProxyPair(relay) {
var pair; var pair;
pair = new ProxyPair(relay, this.rateLimit, this.config.pcConfig); pair = new ProxyPair(relay, this.rateLimit, this.config.pcConfig);
this.proxyPairs.push(pair); this.proxyPairs.push(pair);
pair.onCleanup = (event) => { pair.onCleanup = (event) => {
var ind; var ind;
// Delete from the list of active proxy pairs. // Delete from the list of active proxy pairs.
ind = this.proxyPairs.indexOf(pair); ind = this.proxyPairs.indexOf(pair);
if (ind > -1) { if (ind > -1) {
return this.proxyPairs.splice(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; };
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.relayAddr = null;
Snowflake.prototype.rateLimit = null; Snowflake.prototype.rateLimit = null;
Snowflake.prototype.pollInterval = null; Snowflake.prototype.pollInterval = null;
Snowflake.prototype.retries = 0; Snowflake.prototype.retries = 0;
// Janky state machine // Janky state machine
Snowflake.MODE = { Snowflake.MODE = {
INIT: 0, INIT: 0,
WEBRTC_CONNECTING: 1, WEBRTC_CONNECTING: 1,
WEBRTC_READY: 2 WEBRTC_READY: 2
}; };
Snowflake.MESSAGE = { Snowflake.MESSAGE = {
CONFIRMATION: 'You\'re currently serving a Tor user via Snowflake.' CONFIRMATION: 'You\'re currently serving a Tor user via Snowflake.'
}; };
return Snowflake;
}).call(this);

View file

@ -1,39 +1,32 @@
// Generated by CoffeeScript 2.4.1
/* /*
jasmine tests for Snowflake broker jasmine tests for Snowflake broker
*/ */
var XMLHttpRequest;
XMLHttpRequest = (function() { // fake xhr
// fake xhr // class XMLHttpRequest
// class XMLHttpRequest class XMLHttpRequest {
class XMLHttpRequest { constructor() {
constructor() { this.onreadystatechange = null;
this.onreadystatechange = null; }
} open() {}
setRequestHeader() {}
send() {}
};
open() {} XMLHttpRequest.prototype.DONE = 1;
setRequestHeader() {}
send() {}
};
XMLHttpRequest.prototype.DONE = 1;
return XMLHttpRequest;
}).call(this);
describe('Broker', function() { describe('Broker', function() {
it('can be created', function() { it('can be created', function() {
var b; var b;
b = new Broker('fake'); b = new Broker('fake');
expect(b.url).toEqual('https://fake/'); expect(b.url).toEqual('https://fake/');
return expect(b.id).not.toBeNull(); expect(b.id).not.toBeNull();
}); });
describe('getClientOffer', function() { describe('getClientOffer', function() {
it('polls and promises a client offer', function(done) { it('polls and promises a client offer', function(done) {
var b, poll; var b, poll;
b = new Broker('fake'); b = new Broker('fake');
@ -55,6 +48,7 @@ describe('Broker', function() {
return done(); return done();
}); });
}); });
it('rejects if the broker timed-out', function(done) { it('rejects if the broker timed-out', function(done) {
var b, poll; var b, poll;
b = new Broker('fake'); b = new Broker('fake');
@ -75,7 +69,8 @@ describe('Broker', function() {
return done(); return done();
}); });
}); });
return it('rejects on any other status', function(done) {
it('rejects on any other status', function(done) {
var b, poll; var b, poll;
b = new Broker('fake'); b = new Broker('fake');
// fake timed-out request from broker // fake timed-out request from broker
@ -95,18 +90,20 @@ describe('Broker', function() {
expect(b._xhr.status).toBe(1337); expect(b._xhr.status).toBe(1337);
return done(); return done();
}); });
}); });
}); });
it('responds to the broker with answer', function() { it('responds to the broker with answer', function() {
var b; var b = new Broker('fake');
b = new Broker('fake');
spyOn(b, '_postRequest'); spyOn(b, '_postRequest');
b.sendAnswer('fake id', 123); b.sendAnswer('fake id', 123);
return expect(b._postRequest).toHaveBeenCalledWith('fake id', jasmine.any(Object), 'answer', '123'); expect(b._postRequest).toHaveBeenCalledWith('fake id', jasmine.any(Object), 'answer', '123');
}); });
return it('POST XMLHttpRequests to the broker', function() {
var b; it('POST XMLHttpRequests to the broker', function() {
b = new Broker('fake'); var b = new Broker('fake');
b._xhr = new XMLHttpRequest(); b._xhr = new XMLHttpRequest();
spyOn(b._xhr, 'open'); spyOn(b._xhr, 'open');
spyOn(b._xhr, 'setRequestHeader'); spyOn(b._xhr, 'setRequestHeader');
@ -114,6 +111,7 @@ describe('Broker', function() {
b._postRequest(0, b._xhr, 'test', 'data'); b._postRequest(0, b._xhr, 'test', 'data');
expect(b._xhr.open).toHaveBeenCalled(); expect(b._xhr.open).toHaveBeenCalled();
expect(b._xhr.setRequestHeader).toHaveBeenCalled(); expect(b._xhr.setRequestHeader).toHaveBeenCalled();
return expect(b._xhr.send).toHaveBeenCalled(); expect(b._xhr.send).toHaveBeenCalled();
}); });
}); });

View file

@ -1,8 +1,6 @@
// Generated by CoffeeScript 2.4.1
// Fake snowflake to interact with // Fake snowflake to interact with
var snowflake;
snowflake = { var snowflake = {
ui: new UI, ui: new UI,
broker: { broker: {
sendAnswer: function() {} sendAnswer: function() {}
@ -11,24 +9,25 @@ snowflake = {
}; };
describe('Init', function() { describe('Init', function() {
it('gives a dialog when closing, only while active', function() { it('gives a dialog when closing, only while active', function() {
var msg, silenceNotifications;
silenceNotifications = false; silenceNotifications = false;
snowflake.state = Snowflake.MODE.WEBRTC_READY; snowflake.state = Snowflake.MODE.WEBRTC_READY;
msg = window.onbeforeunload(); var msg = window.onbeforeunload();
expect(snowflake.state).toBe(Snowflake.MODE.WEBRTC_READY); expect(snowflake.state).toBe(Snowflake.MODE.WEBRTC_READY);
expect(msg).toBe(Snowflake.MESSAGE.CONFIRMATION); expect(msg).toBe(Snowflake.MESSAGE.CONFIRMATION);
snowflake.state = Snowflake.MODE.INIT; snowflake.state = Snowflake.MODE.INIT;
msg = window.onbeforeunload(); msg = window.onbeforeunload();
expect(snowflake.state).toBe(Snowflake.MODE.INIT); expect(snowflake.state).toBe(Snowflake.MODE.INIT);
return expect(msg).toBe(null); expect(msg).toBe(null);
}); });
return it('does not give a dialog when silent flag is on', function() {
var msg, silenceNotifications; it('does not give a dialog when silent flag is on', function() {
silenceNotifications = true; silenceNotifications = true;
snowflake.state = Snowflake.MODE.WEBRTC_READY; snowflake.state = Snowflake.MODE.WEBRTC_READY;
msg = window.onbeforeunload(); var msg = window.onbeforeunload();
expect(snowflake.state).toBe(Snowflake.MODE.WEBRTC_READY); expect(snowflake.state).toBe(Snowflake.MODE.WEBRTC_READY);
return expect(msg).toBe(null); expect(msg).toBe(null);
}); });
}); });

View file

@ -1,17 +1,15 @@
// Generated by CoffeeScript 2.4.1
/* /*
jasmine tests for Snowflake proxypair jasmine tests for Snowflake proxypair
*/ */
var MessageEvent, arrayMatching;
// Replacement for MessageEvent constructor. // Replacement for MessageEvent constructor.
// https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/MessageEvent // https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/MessageEvent
MessageEvent = function(type, init) { var MessageEvent = function(type, init) {
return init; return init;
}; };
// Asymmetic matcher that checks that two arrays have the same contents. // Asymmetic matcher that checks that two arrays have the same contents.
arrayMatching = function(sample) { var arrayMatching = function(sample) {
return { return {
asymmetricMatch: function(other) { asymmetricMatch: function(other) {
var _, a, b, i, j, len; var _, a, b, i, j, len;
@ -35,46 +33,57 @@ arrayMatching = function(sample) {
}; };
describe('ProxyPair', function() { describe('ProxyPair', function() {
var config, destination, fakeRelay, pp, rateLimit; var config, destination, fakeRelay, pp, rateLimit;
fakeRelay = Parse.address('0.0.0.0:12345'); fakeRelay = Parse.address('0.0.0.0:12345');
rateLimit = new DummyRateLimit; rateLimit = new DummyRateLimit;
config = new Config; config = new Config;
destination = []; destination = [];
// Using the mock PeerConnection definition from spec/snowflake.spec.coffee. // Using the mock PeerConnection definition from spec/snowflake.spec.coffee.
pp = new ProxyPair(fakeRelay, rateLimit, config.pcConfig); var pp = new ProxyPair(fakeRelay, rateLimit, config.pcConfig);
beforeEach(function() { beforeEach(function() {
return pp.begin(); return pp.begin();
}); });
it('begins webrtc connection', function() { it('begins webrtc connection', function() {
return expect(pp.pc).not.toBeNull(); return expect(pp.pc).not.toBeNull();
}); });
describe('accepts WebRTC offer from some client', function() { describe('accepts WebRTC offer from some client', function() {
beforeEach(function() { beforeEach(function() {
return pp.begin(); return pp.begin();
}); });
it('rejects invalid offers', function() { it('rejects invalid offers', function() {
expect(typeof pp.pc.setRemoteDescription).toBe("function"); expect(typeof pp.pc.setRemoteDescription).toBe("function");
expect(pp.pc).not.toBeNull(); expect(pp.pc).not.toBeNull();
expect(pp.receiveWebRTCOffer({})).toBe(false); expect(pp.receiveWebRTCOffer({})).toBe(false);
return expect(pp.receiveWebRTCOffer({ expect(pp.receiveWebRTCOffer({
type: 'answer' type: 'answer'
})).toBe(false); })).toBe(false);
}); });
return it('accepts valid offers', function() {
it('accepts valid offers', function() {
expect(pp.pc).not.toBeNull(); expect(pp.pc).not.toBeNull();
return expect(pp.receiveWebRTCOffer({ expect(pp.receiveWebRTCOffer({
type: 'offer', type: 'offer',
sdp: 'foo' sdp: 'foo'
})).toBe(true); })).toBe(true);
}); });
}); });
it('responds with a WebRTC answer correctly', function() { it('responds with a WebRTC answer correctly', function() {
spyOn(snowflake.broker, 'sendAnswer'); spyOn(snowflake.broker, 'sendAnswer');
pp.pc.onicecandidate({ pp.pc.onicecandidate({
candidate: null candidate: null
}); });
return expect(snowflake.broker.sendAnswer).toHaveBeenCalled(); expect(snowflake.broker.sendAnswer).toHaveBeenCalled();
}); });
it('handles a new data channel correctly', function() { it('handles a new data channel correctly', function() {
expect(pp.client).toBeNull(); expect(pp.client).toBeNull();
pp.pc.ondatachannel({ pp.pc.ondatachannel({
@ -84,21 +93,25 @@ describe('ProxyPair', function() {
expect(pp.client.onopen).not.toBeNull(); expect(pp.client.onopen).not.toBeNull();
expect(pp.client.onclose).not.toBeNull(); expect(pp.client.onclose).not.toBeNull();
expect(pp.client.onerror).not.toBeNull(); expect(pp.client.onerror).not.toBeNull();
return expect(pp.client.onmessage).not.toBeNull(); expect(pp.client.onmessage).not.toBeNull();
}); });
it('connects to the relay once datachannel opens', function() { it('connects to the relay once datachannel opens', function() {
spyOn(pp, 'connectRelay'); spyOn(pp, 'connectRelay');
pp.client.onopen(); pp.client.onopen();
return expect(pp.connectRelay).toHaveBeenCalled(); expect(pp.connectRelay).toHaveBeenCalled();
}); });
it('connects to a relay', function() { it('connects to a relay', function() {
pp.connectRelay(); pp.connectRelay();
expect(pp.relay.onopen).not.toBeNull(); expect(pp.relay.onopen).not.toBeNull();
expect(pp.relay.onclose).not.toBeNull(); expect(pp.relay.onclose).not.toBeNull();
expect(pp.relay.onerror).not.toBeNull(); expect(pp.relay.onerror).not.toBeNull();
return expect(pp.relay.onmessage).not.toBeNull(); expect(pp.relay.onmessage).not.toBeNull();
}); });
return describe('flushes data between client and relay', function() {
describe('flushes data between client and relay', function() {
it('proxies data from client to relay', function() { it('proxies data from client to relay', function() {
var msg; var msg;
pp.pc.ondatachannel({ pp.pc.ondatachannel({
@ -116,8 +129,9 @@ describe('ProxyPair', function() {
pp.onClientToRelayMessage(msg); pp.onClientToRelayMessage(msg);
pp.flush(); pp.flush();
expect(pp.client.send).not.toHaveBeenCalled(); expect(pp.client.send).not.toHaveBeenCalled();
return expect(pp.relay.send).toHaveBeenCalledWith(arrayMatching([1, 2, 3])); expect(pp.relay.send).toHaveBeenCalledWith(arrayMatching([1, 2, 3]));
}); });
it('proxies data from relay to client', function() { it('proxies data from relay to client', function() {
var msg; var msg;
spyOn(pp.client, 'send'); spyOn(pp.client, 'send');
@ -128,16 +142,19 @@ describe('ProxyPair', function() {
pp.onRelayToClientMessage(msg); pp.onRelayToClientMessage(msg);
pp.flush(); pp.flush();
expect(pp.client.send).toHaveBeenCalledWith(arrayMatching([4, 5, 6])); expect(pp.client.send).toHaveBeenCalledWith(arrayMatching([4, 5, 6]));
return expect(pp.relay.send).not.toHaveBeenCalled(); expect(pp.relay.send).not.toHaveBeenCalled();
}); });
return it('sends nothing with nothing to flush', function() {
it('sends nothing with nothing to flush', function() {
spyOn(pp.client, 'send'); spyOn(pp.client, 'send');
spyOn(pp.relay, 'send'); spyOn(pp.relay, 'send');
pp.flush(); pp.flush();
expect(pp.client.send).not.toHaveBeenCalled(); expect(pp.client.send).not.toHaveBeenCalled();
return expect(pp.relay.send).not.toHaveBeenCalled(); expect(pp.relay.send).not.toHaveBeenCalled();
}); });
}); });
}); });
// TODO: rate limit tests // TODO: rate limit tests

View file

@ -1,62 +1,43 @@
// Generated by CoffeeScript 2.4.1
/* /*
jasmine tests for Snowflake jasmine tests for Snowflake
*/ */
var FakeBroker, PeerConnection, SessionDescription, WebSocket, config, log, ui;
// Fake browser functionality: // Fake browser functionality:
PeerConnection = class PeerConnection { class PeerConnection {
setRemoteDescription() { setRemoteDescription() {
return true; return true;
} }
send(data) {} send(data) {}
}; };
SessionDescription = (function() { class SessionDescription {};
class SessionDescription {}; SessionDescription.prototype.type = 'offer';
SessionDescription.prototype.type = 'offer'; class WebSocket {
constructor() {
this.bufferedAmount = 0;
}
send(data) {}
};
WebSocket.prototype.OPEN = 1;
WebSocket.prototype.CLOSED = 0;
return SessionDescription; var log = function() {};
}).call(this); var config = new Config;
WebSocket = (function() { var ui = new UI;
class WebSocket {
constructor() {
this.bufferedAmount = 0;
}
send(data) {} class FakeBroker {
};
WebSocket.prototype.OPEN = 1;
WebSocket.prototype.CLOSED = 0;
return WebSocket;
}).call(this);
log = function() {};
config = new Config;
ui = new UI;
FakeBroker = class FakeBroker {
getClientOffer() { getClientOffer() {
return new Promise(function(F, R) { return new Promise(function(F, R) {
return {}; return {};
}); });
} }
}; };
describe('Snowflake', function() { describe('Snowflake', function() {
it('constructs correctly', function() { it('constructs correctly', function() {
var s; var s;
s = new Snowflake(config, ui, { s = new Snowflake(config, ui, {
@ -67,22 +48,25 @@ describe('Snowflake', function() {
fake: 'broker' fake: 'broker'
}); });
expect(s.ui).not.toBeNull(); expect(s.ui).not.toBeNull();
return expect(s.retries).toBe(0); expect(s.retries).toBe(0);
}); });
it('sets relay address correctly', function() { it('sets relay address correctly', function() {
var s; var s;
s = new Snowflake(config, ui, null); s = new Snowflake(config, ui, null);
s.setRelayAddr('foo'); s.setRelayAddr('foo');
return expect(s.relayAddr).toEqual('foo'); expect(s.relayAddr).toEqual('foo');
}); });
it('initalizes WebRTC connection', function() { it('initalizes WebRTC connection', function() {
var s; var s;
s = new Snowflake(config, ui, new FakeBroker()); s = new Snowflake(config, ui, new FakeBroker());
spyOn(s.broker, 'getClientOffer').and.callThrough(); spyOn(s.broker, 'getClientOffer').and.callThrough();
s.beginWebRTC(); s.beginWebRTC();
expect(s.retries).toBe(1); expect(s.retries).toBe(1);
return expect(s.broker.getClientOffer).toHaveBeenCalled(); expect(s.broker.getClientOffer).toHaveBeenCalled();
}); });
it('receives SDP offer and sends answer', function() { it('receives SDP offer and sends answer', function() {
var pair, s; var pair, s;
s = new Snowflake(config, ui, new FakeBroker()); s = new Snowflake(config, ui, new FakeBroker());
@ -92,8 +76,9 @@ describe('Snowflake', function() {
spyOn(pair, 'receiveWebRTCOffer').and.returnValue(true); spyOn(pair, 'receiveWebRTCOffer').and.returnValue(true);
spyOn(s, 'sendAnswer'); spyOn(s, 'sendAnswer');
s.receiveOffer(pair, '{"type":"offer","sdp":"foo"}'); s.receiveOffer(pair, '{"type":"offer","sdp":"foo"}');
return expect(s.sendAnswer).toHaveBeenCalled(); expect(s.sendAnswer).toHaveBeenCalled();
}); });
it('does not send answer when receiving invalid offer', function() { it('does not send answer when receiving invalid offer', function() {
var pair, s; var pair, s;
s = new Snowflake(config, ui, new FakeBroker()); s = new Snowflake(config, ui, new FakeBroker());
@ -103,12 +88,14 @@ describe('Snowflake', function() {
spyOn(pair, 'receiveWebRTCOffer').and.returnValue(false); spyOn(pair, 'receiveWebRTCOffer').and.returnValue(false);
spyOn(s, 'sendAnswer'); spyOn(s, 'sendAnswer');
s.receiveOffer(pair, '{"type":"not a good offer","sdp":"foo"}'); s.receiveOffer(pair, '{"type":"not a good offer","sdp":"foo"}');
return expect(s.sendAnswer).not.toHaveBeenCalled(); expect(s.sendAnswer).not.toHaveBeenCalled();
}); });
return it('can make a proxypair', function() {
it('can make a proxypair', function() {
var s; var s;
s = new Snowflake(config, ui, new FakeBroker()); s = new Snowflake(config, ui, new FakeBroker());
s.makeProxyPair(); s.makeProxyPair();
return expect(s.proxyPairs.length).toBe(1); expect(s.proxyPairs.length).toBe(1);
}); });
}); });

View file

@ -1,10 +1,7 @@
// Generated by CoffeeScript 2.4.1
/* /*
jasmine tests for Snowflake UI jasmine tests for Snowflake UI
*/ */
var document; var document = {
document = {
getElementById: function(id) { getElementById: function(id) {
return {}; return {};
}, },
@ -14,6 +11,7 @@ document = {
}; };
describe('UI', function() { describe('UI', function() {
it('activates debug mode when badge does not exist', function() { it('activates debug mode when badge does not exist', function() {
var u; var u;
spyOn(document, 'getElementById').and.callFake(function(id) { spyOn(document, 'getElementById').and.callFake(function(id) {
@ -25,8 +23,9 @@ describe('UI', function() {
u = new DebugUI(); u = new DebugUI();
expect(document.getElementById.calls.count()).toEqual(2); expect(document.getElementById.calls.count()).toEqual(2);
expect(u.$status).not.toBeNull(); expect(u.$status).not.toBeNull();
return expect(u.$msglog).not.toBeNull(); expect(u.$msglog).not.toBeNull();
}); });
it('is not debug mode when badge exists', function() { it('is not debug mode when badge exists', function() {
var u; var u;
spyOn(document, 'getElementById').and.callFake(function(id) { spyOn(document, 'getElementById').and.callFake(function(id) {
@ -38,8 +37,9 @@ describe('UI', function() {
u = new BadgeUI(); u = new BadgeUI();
expect(document.getElementById).toHaveBeenCalled(); expect(document.getElementById).toHaveBeenCalled();
expect(document.getElementById.calls.count()).toEqual(1); expect(document.getElementById.calls.count()).toEqual(1);
return expect(u.$badge).not.toBeNull(); expect(u.$badge).not.toBeNull();
}); });
it('sets status message when in debug mode', function() { it('sets status message when in debug mode', function() {
var u; var u;
u = new DebugUI(); u = new DebugUI();
@ -50,16 +50,18 @@ describe('UI', function() {
} }
}; };
u.setStatus('test'); u.setStatus('test');
return expect(u.$status.innerHTML).toEqual('Status: test'); expect(u.$status.innerHTML).toEqual('Status: test');
}); });
it('sets message log css correctly for debug mode', function() { it('sets message log css correctly for debug mode', function() {
var u; var u;
u = new DebugUI(); u = new DebugUI();
u.setActive(true); u.setActive(true);
expect(u.$msglog.className).toEqual('active'); expect(u.$msglog.className).toEqual('active');
u.setActive(false); u.setActive(false);
return expect(u.$msglog.className).toEqual(''); expect(u.$msglog.className).toEqual('');
}); });
it('sets badge css correctly for non-debug mode', function() { it('sets badge css correctly for non-debug mode', function() {
var u; var u;
u = new BadgeUI(); u = new BadgeUI();
@ -67,9 +69,10 @@ describe('UI', function() {
u.setActive(true); u.setActive(true);
expect(u.$badge.className).toEqual('active'); expect(u.$badge.className).toEqual('active');
u.setActive(false); u.setActive(false);
return expect(u.$badge.className).toEqual(''); expect(u.$badge.className).toEqual('');
}); });
return it('logs to the textarea correctly when debug mode', function() {
it('logs to the textarea correctly when debug mode', function() {
var u; var u;
u = new DebugUI(); u = new DebugUI();
u.$msglog = { u.$msglog = {
@ -79,6 +82,7 @@ describe('UI', function() {
}; };
u.log('test'); u.log('test');
expect(u.$msglog.value).toEqual('test\n'); expect(u.$msglog.value).toEqual('test\n');
return expect(u.$msglog.scrollTop).toEqual(1337); expect(u.$msglog.scrollTop).toEqual(1337);
}); });
}); });

View file

@ -1,10 +1,12 @@
// Generated by CoffeeScript 2.4.1
/* /*
jasmine tests for Snowflake utils jasmine tests for Snowflake utils
*/ */
describe('Parse', function() { describe('Parse', function() {
describe('cookie', function() { describe('cookie', function() {
return it('parses correctly', function() {
it('parses correctly', function() {
expect(Parse.cookie('')).toEqual({}); expect(Parse.cookie('')).toEqual({});
expect(Parse.cookie('a=b')).toEqual({ expect(Parse.cookie('a=b')).toEqual({
a: 'b' a: 'b'
@ -30,12 +32,15 @@ describe('Parse', function() {
expect(Parse.cookie('key=%26%20')).toEqual({ expect(Parse.cookie('key=%26%20')).toEqual({
key: '& ' key: '& '
}); });
return expect(Parse.cookie('a=\'\'')).toEqual({ expect(Parse.cookie('a=\'\'')).toEqual({
a: '\'\'' a: '\'\''
}); });
}); });
}); });
describe('address', function() { describe('address', function() {
it('parses IPv4', function() { it('parses IPv4', function() {
expect(Parse.address('')).toBeNull(); expect(Parse.address('')).toBeNull();
expect(Parse.address('3.3.3.3:4444')).toEqual({ expect(Parse.address('3.3.3.3:4444')).toEqual({
@ -45,9 +50,10 @@ describe('Parse', function() {
expect(Parse.address('3.3.3.3')).toBeNull(); expect(Parse.address('3.3.3.3')).toBeNull();
expect(Parse.address('3.3.3.3:0x1111')).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:-4444')).toBeNull();
return expect(Parse.address('3.3.3.3:65536')).toBeNull(); expect(Parse.address('3.3.3.3:65536')).toBeNull();
}); });
return it('parses IPv6', function() {
it('parses IPv6', function() {
expect(Parse.address('[1:2::a:f]:4444')).toEqual({ expect(Parse.address('[1:2::a:f]:4444')).toEqual({
host: '1:2::a:f', host: '1:2::a:f',
port: 4444 port: 4444
@ -56,15 +62,17 @@ describe('Parse', function() {
expect(Parse.address('[1:2::a:f]:0x1111')).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]:-4444')).toBeNull();
expect(Parse.address('[1:2::a:f]:65536')).toBeNull(); expect(Parse.address('[1:2::a:f]:65536')).toBeNull();
return expect(Parse.address('[1:2::ffff:1.2.3.4]:4444')).toEqual({ expect(Parse.address('[1:2::ffff:1.2.3.4]:4444')).toEqual({
host: '1:2::ffff:1.2.3.4', host: '1:2::ffff:1.2.3.4',
port: 4444 port: 4444
}); });
}); });
}); });
return describe('ipFromSDP', function() {
var testCases; describe('ipFromSDP', function() {
testCases = [
var testCases = [
{ {
// https://tools.ietf.org/html/rfc4566#section-5 // 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", 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",
@ -128,7 +136,8 @@ describe('Parse', function() {
expected: void 0 expected: void 0
} }
]; ];
return it('parses SDP', function() {
it('parses SDP', function() {
var i, len, ref, ref1, results, test; var i, len, ref, ref1, results, test;
results = []; results = [];
for (i = 0, len = testCases.length; i < len; i++) { for (i = 0, len = testCases.length; i < len; i++) {
@ -143,10 +152,13 @@ describe('Parse', function() {
} }
return results; return results;
}); });
}); });
}); });
describe('query string', function() { describe('query string', function() {
it('should parse correctly', function() { it('should parse correctly', function() {
expect(Query.parse('')).toEqual({}); expect(Query.parse('')).toEqual({});
expect(Query.parse('a=b')).toEqual({ expect(Query.parse('a=b')).toEqual({
@ -178,11 +190,12 @@ describe('query string', function() {
expect(Query.parse('a+b=c')).toEqual({ expect(Query.parse('a+b=c')).toEqual({
'a b': 'c' 'a b': 'c'
}); });
return expect(Query.parse('a=b+c+d')).toEqual({ expect(Query.parse('a=b+c+d')).toEqual({
a: 'b c d' a: 'b c d'
}); });
}); });
return it('uses the first appearance of duplicate key', function() {
it('uses the first appearance of duplicate key', function() {
expect(Query.parse('a=b&c=d&a=e')).toEqual({ expect(Query.parse('a=b&c=d&a=e')).toEqual({
a: 'b', a: 'b',
c: 'd' c: 'd'
@ -201,21 +214,24 @@ describe('query string', function() {
a: 'b', a: 'b',
'': '' '': ''
}); });
return expect(Query.parse('a=b&&c=d')).toEqual({ expect(Query.parse('a=b&&c=d')).toEqual({
a: 'b', a: 'b',
'': '', '': '',
c: 'd' c: 'd'
}); });
}); });
}); });
describe('Params', function() { describe('Params', function() {
describe('bool', function() { describe('bool', function() {
var getBool;
getBool = function(query) { var getBool = function(query) {
return Params.getBool(Query.parse(query), 'param', false); return Params.getBool(Query.parse(query), 'param', false);
}; };
return it('parses correctly', function() {
it('parses correctly', function() {
expect(getBool('param=true')).toBe(true); expect(getBool('param=true')).toBe(true);
expect(getBool('param')).toBe(true); expect(getBool('param')).toBe(true);
expect(getBool('param=')).toBe(true); expect(getBool('param=')).toBe(true);
@ -223,19 +239,23 @@ describe('Params', function() {
expect(getBool('param=0')).toBe(false); expect(getBool('param=0')).toBe(false);
expect(getBool('param=false')).toBe(false); expect(getBool('param=false')).toBe(false);
expect(getBool('param=unexpected')).toBeNull(); expect(getBool('param=unexpected')).toBeNull();
return expect(getBool('pram=true')).toBe(false); expect(getBool('pram=true')).toBe(false);
}); });
}); });
return describe('address', function() {
var DEFAULT, getAddress; describe('address', function() {
DEFAULT = {
var DEFAULT = {
host: '1.1.1.1', host: '1.1.1.1',
port: 2222 port: 2222
}; };
getAddress = function(query) {
var getAddress = function(query) {
return Params.getAddress(query, 'addr', DEFAULT); return Params.getAddress(query, 'addr', DEFAULT);
}; };
return it('parses correctly', function() {
it('parses correctly', function() {
expect(getAddress({})).toEqual(DEFAULT); expect(getAddress({})).toEqual(DEFAULT);
expect(getAddress({ expect(getAddress({
addr: '3.3.3.3:4444' addr: '3.3.3.3:4444'
@ -246,9 +266,11 @@ describe('Params', function() {
expect(getAddress({ expect(getAddress({
x: '3.3.3.3:4444' x: '3.3.3.3:4444'
})).toEqual(DEFAULT); })).toEqual(DEFAULT);
return expect(getAddress({ expect(getAddress({
addr: '---' addr: '---'
})).toBeNull(); })).toBeNull();
}); });
}); });
}); });

View file

@ -1,32 +1,39 @@
// Generated by CoffeeScript 2.4.1
/* /*
jasmine tests for Snowflake websocket jasmine tests for Snowflake websocket
*/ */
describe('BuildUrl', function() { describe('BuildUrl', function() {
it('should parse just protocol and host', function() { it('should parse just protocol and host', function() {
return expect(WS.buildUrl('http', 'example.com')).toBe('http://example.com'); expect(WS.buildUrl('http', 'example.com')).toBe('http://example.com');
}); });
it('should handle different ports', function() { it('should handle different ports', function() {
expect(WS.buildUrl('http', 'example.com', 80)).toBe('http://example.com'); 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', 81)).toBe('http://example.com:81');
expect(WS.buildUrl('http', 'example.com', 443)).toBe('http://example.com:443'); 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'); expect(WS.buildUrl('http', 'example.com', 444)).toBe('http://example.com:444');
}); });
it('should handle paths', function() { it('should handle paths', function() {
expect(WS.buildUrl('http', 'example.com', 80, '/')).toBe('http://example.com/'); 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?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'); expect(WS.buildUrl('http', 'example.com', 80, '/test')).toBe('http://example.com/test');
}); });
it('should handle params', function() { it('should handle params', function() {
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', [['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'); 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() { it('should handle ips', function() {
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');
return 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]');
}); });
return it('should handle bogus', function() {
it('should handle bogus', function() {
expect(WS.buildUrl('http', 'bog][us')).toBe('http://bog%5D%5Bus'); expect(WS.buildUrl('http', 'bog][us')).toBe('http://bog%5D%5Bus');
return expect(WS.buildUrl('http', 'bog:u]s')).toBe('http://bog%3Au%5Ds'); expect(WS.buildUrl('http', 'bog:u]s')).toBe('http://bog%3Au%5Ds');
}); });
}); });

View file

@ -1,197 +1,178 @@
// Generated by CoffeeScript 2.4.1 /*
/* All of Snowflake's DOM manipulation and inputs.
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 {
class UI {
setStatus(msg) {}
setActive(connected) { setStatus(msg) {}
return this.active = connected;
setActive(connected) {
return this.active = connected;
}
log(msg) {}
};
UI.prototype.active = false;
UI.prototype.enabled = true;
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;
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);
}
log(msg) {} 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;
}
UI.prototype.active = false; };
UI.prototype.enabled = true; // DOM elements references.
DebugUI.prototype.$msglog = null;
return UI; DebugUI.prototype.$status = null;
}).call(this);
BadgeUI = (function() { class WebExtUI extends UI {
class BadgeUI extends UI {
constructor() {
super();
this.$badge = document.getElementById('badge');
}
setActive(connected) { constructor() {
super.setActive(connected); super();
return this.$badge.className = connected ? 'active' : ''; 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];
BadgeUI.prototype.$badge = null; return setInterval((() => {
this.stats.unshift(0);
return BadgeUI; this.stats.splice(24);
}).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(); return this.postActive();
} }), 60 * 60 * 1000);
}
onMessage(m) { initToggle() {
var storing; var getting;
boundMethodCheck(this, WebExtUI); return getting = chrome.storage.local.get("snowflake-enabled", (result) => {
this.enabled = m.enabled; if (result['snowflake-enabled'] !== void 0) {
this.setEnabled(this.enabled); this.enabled = result['snowflake-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 { } else {
return chrome.browserAction.setIcon({ log("Toggle state not yet saved");
path: {
32: "icons/status-on.png"
}
});
} }
} return this.setEnabled(this.enabled);
});
}
setEnabled(enabled) { postActive() {
update(); 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) {
this.port = port;
port.onDisconnect.addListener(this.onDisconnect);
port.onMessage.addListener(this.onMessage);
return this.postActive();
}
onMessage(m) {
var storing;
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) {
return this.port = null;
}
setActive(connected) {
super.setActive(connected);
if (connected) {
this.stats[0] += 1;
}
this.postActive();
if (this.active) {
return chrome.browserAction.setIcon({ return chrome.browserAction.setIcon({
path: { path: {
32: "icons/status-" + (enabled ? "on" : "off") + ".png" 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; WebExtUI.prototype.port = null;
return WebExtUI; WebExtUI.prototype.stats = null;
}).call(this);

View file

@ -1,53 +1,54 @@
// Generated by CoffeeScript 2.4.1
/* /*
A Coffeescript WebRTC snowflake proxy A Coffeescript WebRTC snowflake proxy
Contains helpers for parsing query strings and other utilities. Contains helpers for parsing query strings and other utilities.
*/ */
var BucketRateLimit, DummyRateLimit, Params, Parse, Query, Util;
Util = (function() { class Util {
class Util {
static mightBeTBB() { static mightBeTBB() {
return Util.TBB_UAS.indexOf(window.navigator.userAgent) > -1 && (window.navigator.mimeTypes && window.navigator.mimeTypes.length === 0); 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.
static genSnowflakeID() { if (Util.mightBeTBB()) {
return Math.random().toString(36).substring(2); log('Will not run within Tor Browser.');
return true;
} }
return false;
}
static snowflakeIsDisabled(cookieName) { static featureDetect() {
var cookies; return typeof PeerConnection === 'function';
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'
];
// 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; class Query {
}).call(this);
Query = class Query {
/* /*
Parse a URL query string or application/x-www-form-urlencoded body. The 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, return type is an object mapping string keys to string values. By design,
@ -100,7 +101,9 @@ Query = class Query {
}; };
Parse = class Parse {
class Parse {
// Parse a cookie data string (usually document.cookie). The return type is an // Parse a cookie data string (usually document.cookie). The return type is an
// object mapping cookies names to values. Returns null on error. // object mapping cookies names to values. Returns null on error.
// http://www.w3.org/TR/DOM-Level-2-HTML/html.html#ID-8747038 // http://www.w3.org/TR/DOM-Level-2-HTML/html.html#ID-8747038
@ -202,7 +205,9 @@ Parse = class Parse {
}; };
Params = class Params {
class Params {
static getBool(query, param, defaultValue) { static getBool(query, param, defaultValue) {
var val; var val;
val = query[param]; val = query[param];
@ -254,53 +259,52 @@ Params = class Params {
}; };
BucketRateLimit = (function() {
class BucketRateLimit { class BucketRateLimit {
constructor(capacity, time) {
this.capacity = capacity; constructor(capacity, time) {
this.time = 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;
} }
}
age() { update(n) {
var delta, now; this.age();
now = new Date(); this.amount += n;
delta = (now - this.lastUpdate) / 1000.0; return this.amount <= this.capacity;
this.lastUpdate = now; }
this.amount -= delta * this.capacity / this.time;
if (this.amount < 0.0) {
return this.amount = 0.0;
}
}
update(n) { // How many seconds in the future will the limit expire?
this.age(); when() {
this.amount += n; this.age();
return this.amount <= this.capacity; return (this.amount - this.capacity) / (this.capacity / this.time);
} }
// How many seconds in the future will the limit expire? isLimited() {
when() { this.age();
this.age(); return this.amount > this.capacity;
return (this.amount - this.capacity) / (this.capacity / this.time); }
}
isLimited() { };
this.age();
return this.amount > this.capacity;
}
}; BucketRateLimit.prototype.amount = 0.0;
BucketRateLimit.prototype.amount = 0.0; BucketRateLimit.prototype.lastUpdate = new Date();
BucketRateLimit.prototype.lastUpdate = new Date();
return BucketRateLimit;
}).call(this);
// A rate limiter that never limits. // A rate limiter that never limits.
DummyRateLimit = class DummyRateLimit { class DummyRateLimit {
constructor(capacity, time) { constructor(capacity, time) {
this.capacity = capacity; this.capacity = capacity;
this.time = time; this.time = time;

View file

@ -1,70 +1,64 @@
// Generated by CoffeeScript 2.4.1
/* /*
Only websocket-specific stuff. Only websocket-specific stuff.
*/ */
var WS;
WS = (function() { class WS {
class WS {
// Build an escaped URL string from unescaped components. Only scheme and host // Build an escaped URL string from unescaped components. Only scheme and host
// are required. See RFC 3986, section 3. // are required. See RFC 3986, section 3.
static buildUrl(scheme, host, port, path, params) { static buildUrl(scheme, host, port, path, params) {
var parts; var parts;
parts = []; parts = [];
parts.push(encodeURIComponent(scheme)); parts.push(encodeURIComponent(scheme));
parts.push('://'); parts.push('://');
// If it contains a colon but no square brackets, treat it as IPv6. // If it contains a colon but no square brackets, treat it as IPv6.
if (host.match(/:/) && !host.match(/[[\]]/)) { if (host.match(/:/) && !host.match(/[[\]]/)) {
parts.push('['); parts.push('[');
parts.push(host); parts.push(host);
parts.push(']'); parts.push(']');
} else { } else {
parts.push(encodeURIComponent(host)); 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('');
} }
if (void 0 !== port && this.DEFAULT_PORTS[scheme] !== port) {
static makeWebsocket(addr, params) { parts.push(':');
var url, ws, wsProtocol; parts.push(encodeURIComponent(port.toString()));
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;
} }
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 = { WS.WSS_ENABLED = true;
http: 80,
https: 443
};
return WS; WS.DEFAULT_PORTS = {
http: 80,
}).call(this); https: 443
};