diff --git a/.gitignore b/.gitignore
index a187011..315500c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,5 +15,9 @@ proxy/build
proxy/node_modules
proxy/spec/support
proxy/webext/snowflake.js
+proxy/webext/popup.js
+proxy/webext/embed.html
+proxy/webext/embed.css
+proxy/webext/icons/
ignore/
npm-debug.log
diff --git a/proxy/init-badge.js b/proxy/init-badge.js
index 8646bc4..f85be29 100644
--- a/proxy/init-badge.js
+++ b/proxy/init-badge.js
@@ -1,18 +1,71 @@
/* global TESTING, Util, Params, Config, DebugUI, BadgeUI, UI, Broker, Snowflake */
+/*
+UI
+*/
+
+class BadgeUI extends UI {
+
+ constructor() {
+ super();
+ this.popup = new Popup();
+ }
+
+ setStatus() {}
+
+ missingFeature(missing) {
+ this.popup.setImgSrc('off');
+ this.popup.setStatusText("Snowflake is off");
+ this.popup.setStatusDesc(missing, 'firebrick');
+ this.popup.hideButton();
+ }
+
+ turnOn() {
+ const clients = this.active ? 1 : 0;
+ this.popup.setChecked(true);
+ this.popup.setToggleText('Turn Off');
+ this.popup.setStatusText(`${clients} client${(clients !== 1) ? 's' : ''} connected.`);
+ // FIXME: Share stats from webext
+ const total = 0;
+ this.popup.setStatusDesc(`Your snowflake has helped ${total} user${(total !== 1) ? 's' : ''} circumvent censorship in the last 24 hours.`);
+ this.popup.setImgSrc(this.active ? "running" : "on");
+ }
+
+ turnOff() {
+ this.popup.setChecked(false);
+ this.popup.setToggleText('Turn On');
+ this.popup.setStatusText("Snowflake is off");
+ this.popup.setStatusDesc("");
+ this.popup.setImgSrc("off");
+ }
+
+ setActive(connected) {
+ super.setActive(connected);
+ turnOn();
+ }
+
+}
+
+BadgeUI.prototype.popup = null;
+
+
/*
Entry point.
*/
-var snowflake, query, debug, silenceNotifications, log, dbg, init;
+// Defaults to opt-in.
+var COOKIE_NAME = "snowflake-allow";
+var COOKIE_LIFETIME = "Thu, 01 Jan 2038 00:00:00 GMT";
+var COOKIE_EXPIRE = "Thu, 01 Jan 1970 00:00:01 GMT";
+
+function setSnowflakeCookie(val, expires) {
+ document.cookie = `${COOKIE_NAME}=${val}; path=/; expires=${expires};`;
+}
+
+var debug, snowflake, config, broker, ui, log, dbg, init, update, silenceNotifications, query;
(function() {
- if (((typeof TESTING === "undefined" || TESTING === null) || !TESTING) && !Util.featureDetect()) {
- console.log('webrtc feature not detected. shutting down');
- return;
- }
-
snowflake = null;
query = new URLSearchParams(location.search);
@@ -35,32 +88,56 @@ var snowflake, query, debug, silenceNotifications, log, dbg, init;
}
};
+ update = function() {
+ const cookies = Parse.cookie(document.cookie);
+ if (cookies[COOKIE_NAME] === '1') {
+ ui.turnOn();
+ dbg('Contacting Broker at ' + broker.url);
+ log('Starting snowflake');
+ snowflake.setRelayAddr(config.relayAddr);
+ snowflake.beginWebRTC();
+ } else {
+ ui.turnOff();
+ snowflake.disable();
+ log('Currently not active.');
+ }
+ };
+
init = function() {
- var broker, config, ui;
+ ui = new BadgeUI();
+
+ if (!Util.hasWebRTC()) {
+ ui.missingFeature("WebRTC feature is not detected.");
+ return;
+ }
+
+ if (!Util.hasCookies()) {
+ ui.missingFeature("Cookies are not enabled.");
+ return;
+ }
+
+ if (Util.mightBeTBB()) {
+ ui.missingFeature("Will not run within Tor Browser.");
+ return;
+ }
+
config = new Config;
if ('off' !== query.get('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();
+ update();
+
+ document.getElementById('enabled').addEventListener('change', (event) => {
+ if (event.target.checked) {
+ setSnowflakeCookie('1', COOKIE_LIFETIME);
+ } else {
+ setSnowflakeCookie('', COOKIE_EXPIRE);
+ }
+ update();
+ })
};
// Notification of closing tab with active proxy.
diff --git a/proxy/init-testing.js b/proxy/init-testing.js
new file mode 100644
index 0000000..003f2b6
--- /dev/null
+++ b/proxy/init-testing.js
@@ -0,0 +1,83 @@
+/* global TESTING, Util, Params, Config, DebugUI, UI, Broker, Snowflake */
+
+/*
+Entry point.
+*/
+
+var snowflake, query, debug, silenceNotifications, log, dbg, init;
+
+(function() {
+
+ if (((typeof TESTING === "undefined" || TESTING === null) || !TESTING) && !Util.featureDetect()) {
+ console.log('webrtc feature not detected. shutting down');
+ return;
+ }
+
+ snowflake = null;
+
+ query = new URLSearchParams(location.search);
+
+ 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('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 !== null &&
+ Snowflake.MODE.WEBRTC_READY === snowflake.state
+ ) {
+ return Snowflake.MESSAGE.CONFIRMATION;
+ }
+ return null;
+ };
+
+ window.onunload = function() {
+ if (snowflake !== null) { snowflake.disable(); }
+ return null;
+ };
+
+ window.onload = init;
+
+}());
diff --git a/proxy/init-webext.js b/proxy/init-webext.js
index c641621..df618e6 100644
--- a/proxy/init-webext.js
+++ b/proxy/init-webext.js
@@ -1,6 +1,110 @@
/* global Util, chrome, Config, WebExtUI, Broker, Snowflake */
/* eslint no-unused-vars: 0 */
+/*
+UI
+*/
+
+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() {
+ 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");
+ }
+ 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) {
+ this.port = port;
+ port.onDisconnect.addListener(this.onDisconnect);
+ port.onMessage.addListener(this.onMessage);
+ return this.postActive();
+ }
+
+ onMessage(m) {
+ this.enabled = m.enabled;
+ this.setEnabled(this.enabled);
+ this.postActive();
+ chrome.storage.local.set({
+ "snowflake-enabled": this.enabled
+ }, function() {
+ log("Stored toggle state");
+ });
+ }
+
+ onDisconnect() {
+ 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;
+
/*
Entry point.
*/
@@ -30,7 +134,7 @@ var debug, snowflake, config, broker, ui, log, dbg, init, update, silenceNotific
}
};
- if (!Util.featureDetect()) {
+ if (!Util.hasWebRTC()) {
chrome.runtime.onConnect.addListener(function(port) {
return port.postMessage({
missingFeature: true
diff --git a/proxy/make.js b/proxy/make.js
index 5d6e013..52ee098 100755
--- a/proxy/make.js
+++ b/proxy/make.js
@@ -26,14 +26,19 @@ var FILES_SPEC = [
'spec/websocket.spec.js'
];
-var OUTFILE = 'snowflake.js';
-
var STATIC = 'static';
-var concatJS = function(outDir, init) {
+var SHARED_FILES = [
+ 'embed.html',
+ 'embed.css',
+ 'popup.js',
+ 'icons'
+];
+
+var concatJS = function(outDir, init, outFile) {
var files;
files = FILES.concat(`init-${init}.js`);
- return exec(`cat ${files.join(' ')} > ${outDir}/${OUTFILE}`, function(err) {
+ return exec(`cat ${files.join(' ')} > ${outDir}/${outFile}`, function(err) {
if (err) {
throw err;
}
@@ -53,7 +58,7 @@ task('test', 'snowflake unit tests', function() {
exec('mkdir -p test');
exec('jasmine init >&-');
// Simply concat all the files because we're not using node exports.
- jasmineFiles = FILES.concat('init-badge.js', FILES_SPEC);
+ jasmineFiles = FILES.concat('init-testing.js', FILES_SPEC);
outFile = 'test/bundle.spec.js';
exec('echo "TESTING = true" > ' + outFile);
exec('cat ' + jasmineFiles.join(' ') + ' | cat >> ' + outFile);
@@ -68,19 +73,20 @@ task('test', 'snowflake unit tests', function() {
task('build', 'build the snowflake proxy', function() {
exec('rm -r build');
exec('cp -r ' + STATIC + '/ build/');
- concatJS('build', 'badge');
+ concatJS('build', 'badge', 'embed.js');
console.log('Snowflake prepared.');
});
task('webext', 'build the webextension', function() {
exec('mkdir -p webext');
- concatJS('webext', 'webext');
+ exec(`cp -r ${STATIC}/{${SHARED_FILES.join(',')}} webext/`);
+ concatJS('webext', 'webext', 'snowflake.js');
console.log('Webextension prepared.');
});
task('node', 'build the node binary', function() {
exec('mkdir -p build');
- concatJS('build', 'node');
+ concatJS('build', 'node', 'snowflake.js');
console.log('Node prepared.');
});
diff --git a/proxy/static/.htaccess b/proxy/static/.htaccess
index 3dd217d..f733194 100644
--- a/proxy/static/.htaccess
+++ b/proxy/static/.htaccess
@@ -1,4 +1,3 @@
Snowflake is off
+It is now possible to embed the Snowflake badge on any website:
+ + + +Which looks like this:
+ + +